谈一下我们是怎么做数据库单元测试(Database Unit Test)的

背景介绍

最近在团队在做release之前的regression,把各个feature分支merge回master之后发现DB的单元测试出现了20多个失败的test cases。之前没怎么做过DB的单元测试,正好借这个机会熟悉一下写DB单元测试的流程。

这篇博文中首先介绍一下在我们的特定项目场景中是如何搭建DB 单元测试框架的,然后举一个简单的例子,从头到尾在visual studio中创建一个简单的单元测试工程。

我们开发的产品使用的数据库为Sql Server,总共有400多张表,2000多个存储过程,每个存储过程都相当于应用代码中的一个功能函数。代码中的每个复杂的功能函数都可以通过写单元测试来在一定程度上保证代码质量,存储过程也如此。代码中的UT难点在于解耦,也就把相互牵连在一起的代码彼此分离开来,各个击破,例如A函数需要B函数提供的数据,测试A函数的时候我们只想测试A函数,不想调用B,这时候就需要我们自己提供B函数生成的数据。这叫做mock。

在做DB单元测试的时候,存储过程所使用的数据比较特殊,都是持久化在数据库表中的,2000多个存储过程增删改查400多个表,我们需要把这些表的数据为每个存储过程做隔离,如果测试用例使用的数据相互之间关联,恐怕会天下大乱,因为在一般情况下,单元测试用例的运行顺序都是随机的,如果单元测试使用的数据有关联,很有可能两次运行结果也是随机的(但是有一种方法可以固定case执行顺序,我在最后的例子中进行说明),我们这次的20多个失败的cases就有这种原因导致的,两台机器上跑出的结果不一样,有的成功,有的失败。

注:有关单元测试的定义,见另外一篇帖子,单元测试有毒

那么问题就来了,如何才能做数据的隔离呢?说一下我们的方案。


准备数据

我们创建了一个基准的数据库,做出一个备份,叫做base.bak,这个版本比较低,比如是2.8,这里面包含了一些测试的基本数据。然后我们创建了另外一个preparation的工程,用于把base.bak升级到当前release版本,例如,当前release的版本为2.18。这个工程同时也测试了升级的流程。升级成功之后,把这个数据库在本地做一个备份release_2_18.bak。好了,数据都准备好了。



测试需要注意的要点

四个函数

对于微软的这个DB UT测试框架,有四个函数需要搞清楚,因为这可能影响你的测试结果:

[ClassInitialize]public static void ClassInitialize(TestContext testContext){...
}
[ClassCleanup]public static void ClassCleanup(){...
}
[TestInitialize()]public void TestInitialize(){...
}
[TestCleanup()]public void TestCleanup(){            ...
}
  • 顾名思义,ClassInitialize() 是在每个类初始化的时候被调用的

  • ClassCleanup() 是在类结束的时候,也就是一个类所有的case跑完的时候被调用的

  • TestInitialize() 是在每个case跑之前被调用的。

  • TestCleanup() 是在每个case调用之后被调用的。

对么?粗体的这句话不对,其余是对的。

测试用例的运行是无序的,包含多个类的情况。

看下面测试用例的之情情况你就明白了:

AssemblyInitialize

TestClass1: ClassInitialize

TestClass1: TestInitialize

TestClass1: MyTestCase1

TestClass1: TestCleanup

TestClass2: ClassInitialize

TestClass2: TestInitialize

TestClass2: MyTestCase2

TestClass2: TestCleanup

TestClass1: ClassCleanup

TestClass2: ClassCleanup

AssemblyCleanup

ClassCleanup() 并不意味着TestClass1ClassCleanup 在这个类的最后一个case跑完之后被立即调用!事实上,它会等待所有case都被运行完之后,同TestClass2ClassCleanup 一块执行。

具体原因看这个帖子,How to run ClassCleanup (MSTest) after each class with test?

三个Action

还是看下面的一个例子:

[TestMethod()]public void Test_GetBasicRevenueByName(){SqlDatabaseTestActions testActions = this.SqlTest1Data;    
     // Execute the pre-test script// System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script...");SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction);    // Execute the test script// System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script...");SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction);    // Execute the post-test script// System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script...");SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); }

每个测试用例中都会有三个action,这三个Action的用途如下:

  • PretestAction做的是测试前的准备工作,具体过程中可以为每个特定的case插入或更新测试需要的数据。

  • TestAction为调用存储过程进行测试,将实际结果和预期结果进行对比。

  • PosttestAction做的是测试完成后的清理工作,这里可以对PretestAction中的插入或者更新的数据进行回滚,恢复初始环境

最后的这个PosttestAction为我们的数据隔离提供了一种方法,所谓恢复初始环境的意思是执行一个case之前和之后数据库中的数据完全一样。

这里有个问题,在PretestAction中进行数据插入还比较好恢复,如果是删除和更新呢?这就需要你记录下删除的和更新前的数据。太麻烦了。如果你的系统性能足够好,或者对运行UT的时间没有要求,可以用另外一种方法:restore DB。前面不是说过了么,我们在数据库升级之后做了一个备份,我们在这里使用它。在什么地方执行restoreDB?对,在TestCleanup() 中进行。

[TestInitialize()]public void TestCleanup(){restoreDB();
}



总结

具体的流程就说完了,总结一下:

准备数据库

运行测试用例流程

数据清理的两种方法

  • 在PretestAction中添加数据恢复语句;

  • TestCleanup()中restore DB。


实例

接下来我们从头到尾演示一下用VS2013 + SQL Server 2012是如何做数据库UT的。

创建一个简单的数据库DBUTDemo

  • 创建两张表。

create table EmployeeBasicInfo(EmployeeNo int NOT NULL primary key,   Name nvarchar(50) NOT NULL,TelephoneNum varchar(50) NOT NULL  );create table EmployeeRevenue(EmployeeNo int NOT NULL primary key,BasicRevenue int NOT NULL,MealSubsidy int NULL,Bonus int NULL,foreign key(EmployeeNo) references EmployeeBasicInfo(EmployeeNo)  
);
  • 创建一个存储过程

create procedure GetBasicRevenueByName(@name nvarchar(50))  asbeginselect bi.Name,r.BasicRevenue from EmployeeRevenue r 
join EmployeeBasicInfo bi on r.EmployeeNo = bi.EmployeeNo where bi.Name = @nameend

创建UT工程

  • 点击File->New->Project...

  • 选择Unit Test Project,输入工程名,选择创建路径,点击OK

添加一个类

  • 右键DBUTDemo->Add->New Item...

    选择SQL Server Unit Test,输入名字,点击Add。

  • 第一次添加数据库测试类需要配置数据库:
    点击New Connection

输入Server name,选择我们刚才创建的数据库DBUTDemo,点击Test Connection。如果成功会弹出对话框。连续两次点击OK。数据库配置就完成了。

创建三个Actions

点击Click here to create来创建TestAction,点击之后发现多了一个resx文件。

输入下面的测试代码:

declare @return_value  int,@name  nvarchar(50)EXEC    @return_value = [dbo].[GetBasicRevenueByName]@name = N'three zhang'SELECT  'Return Value' = @return_value

接下来创建另外两个Action:

分别输入如下代码:

insert into EmployeeBasicInfo values(1,'three zhang',    '16625344257')insert into EmployeeBasicInfo values(2,'four li',   '16625344258')insert into EmployeeBasicInfo values(3,'simon', '16625344259')insert into EmployeeBasicInfo values(4,'jack',  '16625344250')insert into EmployeeRevenue values(1    ,30000  ,500    ,20000)insert into EmployeeRevenue values(2    ,28000  ,500    ,19000)insert into EmployeeRevenue values(3    ,27000  ,500    ,10000)insert into EmployeeRevenue values(4    ,26000  ,500    ,20000)
delete from EmployeeRevenuedelete from EmployeeBasicInfo

最后添加测试条件


我们添加了两个测试条件,值可以在属性界面中修改:
第一个测试条件是在返回结果集1中,第一行第二列的期望值为30000,也就是three zhang的基本工资为30000。

第二个测试条件测试结果集1非空。

编译,运行

编译成功后,打开Test Explorer,run我们刚才创建的case,测试通过。

Ordered Test

最后说下数据库测试用例如果需要固定的顺序该怎么办,微软提供了一种测试用例类型叫做Ordered Test:

这种case是把几个case集合成为了一个,可以自己选择需要运行的普通的case,自己指定顺序。因为顺序固定了,这些cases中使用的数据就是可控的,因此在一个ordered case中的几个case可以共同使用某些数据,我们可以将数据隔离的单位由单个case变为几个case甚至一个类中的所有cases。

原文地址:http://www.cnblogs.com/harlanc/p/7007145.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/325176.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaFX官方教程(十)之转换类型和示例

翻译自 Transformation Types and Examples 本文档描述了特定的转换并提供了代码示例。 转换 平移变换沿着相对于其初始位置的一个轴将节点从一个位置移动到另一个位置。木琴条的初始位置由x,y和z坐标定义。在实施例2-1中,初始位置值由指定的xStart&a…

Web前端知识体系精简

Web前端技术由html、css和javascript三大部分构成,是一个庞大而复杂的技术体系,其复杂程度不低于任何一门后端语言。而我们在学习它的时候往往是先从某一个点切入,然后不断地接触和学习新的知识点,因此对于初学者很难理清楚整个体…

JavaFX官方教程(十一)之动画基础

翻译自 动画基础 动画基础提供基本动画概念,包含以下部分: 转变 时间线动画 插值 JavaFX中的动画可以分为时间轴动画和过渡。本章提供了每种动画类型的示例。 Timeline并且Transition是javafx.animation.Animation该类的子类。有关特定类&#xff…

内存VS硬盘

一个程序和它的数据在被CPU执行前必须移到计算机的内存 中。 原因 内存存取数据的速度比硬盘的存取速度快10倍,在某些环境里,硬盘和内存之间的速度差距可能会更大。而CPU的速度比内存不知还要快多少倍。当我们把程序从硬盘放到内存以后,CPU…

依赖注入之Autofac使用总结

依赖倒置?控制反转(IOC)? 依赖注入(DI)? 你是否还在被这些名词所困扰,是否看了大量理论文章后还是一知半解了? 今天我想结合实际项目,和正在迷惑中的新手朋友一起来学…

JavaFX官方教程(十二)之树动画示例

翻译自 树动画示例 本章提供有关树动画示例的详细信息。您将了解场景中的所有元素是如何创建和动画的。 图4-1显示了带树的场景。 图4-1树动画 项目和要素 树动画项目由几个文件组成。每个元素,如树叶,草叶等,都是在不同的类中创建的。在…

像素密度(衡量屏幕显示能力)

像素密度√[(长度像素数)^2(宽度像素数)^2]/屏幕尺寸 eg:

通过Roslyn构建自己的C#脚本(更新版)

之前写过文章介绍过如何通过Roslyn构建自己的C#脚本,但那篇文章是参考自Roslyn CTP版的,记得本来想等到Roslyn正式版出来重新更新一下文档的,不过记得后来Roslyn是跳票了的,Scripting API在正式版本中都一度被移除了,这…

JavaFX官方教程(十三)之应用效果

翻译自 Applying Effects 创建视觉效果包含以下主题: 混合效果 绽放效果 模糊效果 投影效果 内阴影效果 反射 照明效果 透视效果 创建一系列效果 介绍如何使用视觉效果来增强JavaFX应用程序的外观。 所有效果都位于javafx.scene.effect包中,…

java中遍历树形菜单,你可能不知道还有这样的方法

版权声明:本文为CSDN博主「穆雄雄」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_34137397/article/details/72654955穆雄雄开发工具:MyEclipse 10.5后台…

Docker-Compose 一键部署Ningx+.Net Core+Redis集群

在看该文章前,你需要对Docker有所了解。 1、创建WebApp应用程序 我使用的是.Net Core 1.0.1版本,创建一个MVC应用程序,并添加对Redis的引用。因为这些很基础,也很简单,这里就不详细说明了,特别提一下有关多…

Catalog Service - 解析微软微服务架构eShopOnContainers(三)

上一篇我们说了Identity Service,因为其基于IdentityServer4开发的,所以知识点不是很多,今天我们来看下Catalog Service,今后的讲解都会把不同的、重点的拿出来讲,希望大家明白。 源码分析 我们先看下它的目录结构&a…

配置环境变量 path

原理 根据windows系统在查找可执行程序的原理,可以将java工具所在路径定义到path 环境变量中,让系统帮我们去找运行执行的程序。 配置方法 我的电脑–属性–高级系统设置–环境变量 编辑 path 环境变量,在变量值开始处加上java工具所在目录…

前端框架选型

前面的话 有一个流传较广的笑话,一个人在stackoverflow中提了一个问题,如何使用javascript实现一个数字与另外一个数字相加。最高票回答是你应该使用jQuery插件,jQuery插件可以做任何事情。 历史总是在重演,以前是jQuery&#xff…

断言、触发器、存储过程

断言 assertion 设置每一门课程最多有60人选修 create assertion a check (60 > all ( select count(*) from sc group by cno) ); drop assertion a; 触发器 trigger 删除触发器 drop tigger a on student 存储过程 定义一个没有返回值的存储过程 create procedure my…

一个java源文件中可以声明多少个class与编译后会生成多少个字节码文件

在一个java源文件中可以声明多个class。 但是,只能最多有一个类声明为public的。 而且要求声明为public的类的类名必须与源文件名相同。 编译的过程 编译以后,会生成一个或多个字节码文件。字节码文件的文件名与java源文件中的类名相同。 运行 只能运…

JavaFX UI控件教程(二)之JavaFX UI控件

翻译自 JavaFX UI控件 本章概述了通过API提供的JavaFX UI控件。 JavaFX UI控件是使用场景图中的节点构建的。因此,控件可以使用JavaFX平台的视觉丰富功能。由于JavaFX API完全用Java实现,因此您可以轻松地将JavaFX UI控件集成到现有的Java应用程序中…

JavaFX UI控件教程(三)之Label

翻译自 Label 本章介绍如何使用Label驻留在javafx.scene.controlJavaFX API包中的类来显示文本元素。了解如何包装文本元素以适合特定空间,添加图形图像或应用视觉效果。 图2-1显示了三种常见的标签用法。左侧的标签是带有图像的文本元素,中间的标签表…

asp.net core中负载均衡场景下http重定向https的问题

上周欣喜地发现,微软官方终于针对 asp.net core 在使用负载均衡的情况下从 http 强制重定向至 https 的问题提供了解决方法。 app.UseForwardedHeaders(new ForwardedHeadersOptions {ForwardedHeaders ForwardedHeaders.XForwardedProto });var options new Rew…