作者水平有限,如有错误或纰漏,请指出,谢谢。

背景介绍

最近在团队在做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))
as
begin
select bi.Name,r.BasicRevenue from EmployeeRevenue r join EmployeeBasicInfo bi on r.EmployeeNo = bi.EmployeeNo where bi.Name = @name
end

创建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 EmployeeRevenue
delete 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。

谈一下我们是怎么做数据库单元测试(Database Unit Test)的的更多相关文章

  1. 从 SDWebImage 谈如何为开源软件做贡献

    来源:伯乐在线 - 酷酷的哀殿 链接:http://ios.jobbole.com/89483/ 点击 → 申请加入伯乐在线专栏作者 从 SDWebImage 谈如何为开源软件做贡献 相识 – 知我者 ...

  2. karma、jasmine做angularjs单元测试

    引用文:karma.jasmine做angularjs单元测试 karma和jasmine介绍 <1>技术介绍 karma karma是Testacular的新名字 karma是用来自动化 ...

  3. 探究Go-YCSB做数据库基准测试

    本篇文章开篇会介绍一下Go-YCSB是如何使用,然后按照惯例会分析一下它是如何做基准测试,看看它有什么优缺点. 转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.l ...

  4. Oracle 数据库(oracle Database)Select 多表关联查询方式

    Oracle数据库中Select语句语法及介绍 SELECT [ ALL | DISTINCT ] <字段表达式1[,<字段表达式2[,…] FROM <表名1>,<表名 ...

  5. 数据库(Database)

    一.定义 1. 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库,简单来说是本身可视为电子化的件柜--存储电子文件的处所,用户可以对文件中的数据进行新增.截取.更新.删除等操作.数 ...

  6. 图数据库(graph database)资料收集和解析 - daily

    Motivation 图数据库中的高科技和高安全性中引用了一个关于图数据库(graph database)的应用前景的乐观估计: 预计到2017年,图数据库产业在数据库市场的份额将从2个百分点增长到2 ...

  7. SQL Server中模式(schema)、数据库(database)、表(table)、用户(user)之间的关系

    数据库的初学者往往会对关系型数据库模式(schema).数据库(database).表(table).用户(user)之间感到迷惘,总感觉他们的关系千丝万缕,但又不知道他们的联系和区别在哪里,对一些问 ...

  8. 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生

    [转].NET(C#):浅谈程序集清单资源和RESX资源   目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...

  9. Hibernate和jsp做数据库单表的增删改查

    虽然很基础,但是我转牛角尖了~~~~这是几个文件 1.最重要的.Java文件 package com.chinasofti.hibb.struts; import org.hibernate.Sess ...

随机推荐

  1. 修改nagios密码和遇到的问题

    htpasswd -c /usr/local/nagios/etc/htpasswd.users nagiosadmin 密码 密码 service httpd restart 由于本屌丢掉一个s使/ ...

  2. XSS跨站脚本攻击

    1.简介 跨站脚本(cross site script)为了避免与样式css混淆,所以简称为XSS. XSS是一种经常出现在web应用中的计算机安全漏洞,也是web中最主流的攻击方式.那么什么是XSS ...

  3. git 利用分支概念实现一个仓库管理两个项目

    需求描述:开发了一个网站,上线之际,突然另一个客户说也想要个一样的网站,但网站的logo和内部展示图片需要替换一下,也就是说大部分的后台业务逻辑代码都是一致的,以后升级时功能也要保持一致:刚开始想反正 ...

  4. javascript原生方法实现extend

    var extend = (function () { for(var p in {toString:null}){ //检查当前浏览器是否支持forin循环去遍历出一个不可枚举的属性,比如toStr ...

  5. HTTP协议详解以及URL具体访问过程

    1.简介 1.1.HTTP协议是什么? 即超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准.从 ...

  6. duboo解析的入口

    使用duboo只需要在spring配置文件做如下配置就好: < dubbo:provider timeout= "${default.dubbo.provider.timeout}&q ...

  7. Github开源:Sheng.RabbitMQ.CommandExecuter (RabbitMQ 的命令模式实现)

    [Github]:https://github.com/iccb1013/Sheng.RabbitMQ.CommandExecuter Sheng.RabbitMQ.CommandExecuter 是 ...

  8. Centos7通过Docker安装Sentry(哨兵)

    Docker介绍 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化.容器是完全使用沙箱机制, ...

  9. Vmware虚拟机中安装cnetOS7详细图解步骤

    1.安装VMware 下载一个软件安装: .新建一个虚拟机 .引用安装包 4.启动新建的虚拟机 .安装CentOS7的步骤 配置系统语言: 配置系统时间: 配置系统键盘: 配置键盘切换的快捷键: 配置 ...

  10. 【安装Python环境】之“安装 setuptools ”时出现的问题以及解决办法

    安装Python环境时,还需要安装"setuptools 与 pip",但是安装setuptools时出现了几个问题,如下: setuptools 与 pip 下载地址如下:htt ...