谈一下我们是怎么做数据库单元测试(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() 并不意味着TestClass1 的ClassCleanup 在这个类的最后一个case跑完之后被立即调用!事实上,它会等待所有case都被运行完之后,同TestClass2 的ClassCleanup 一块执行。
具体原因看这个帖子,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)的的更多相关文章
- 从 SDWebImage 谈如何为开源软件做贡献
来源:伯乐在线 - 酷酷的哀殿 链接:http://ios.jobbole.com/89483/ 点击 → 申请加入伯乐在线专栏作者 从 SDWebImage 谈如何为开源软件做贡献 相识 – 知我者 ...
- karma、jasmine做angularjs单元测试
引用文:karma.jasmine做angularjs单元测试 karma和jasmine介绍 <1>技术介绍 karma karma是Testacular的新名字 karma是用来自动化 ...
- 探究Go-YCSB做数据库基准测试
本篇文章开篇会介绍一下Go-YCSB是如何使用,然后按照惯例会分析一下它是如何做基准测试,看看它有什么优缺点. 转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.l ...
- Oracle 数据库(oracle Database)Select 多表关联查询方式
Oracle数据库中Select语句语法及介绍 SELECT [ ALL | DISTINCT ] <字段表达式1[,<字段表达式2[,…] FROM <表名1>,<表名 ...
- 数据库(Database)
一.定义 1. 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库,简单来说是本身可视为电子化的件柜--存储电子文件的处所,用户可以对文件中的数据进行新增.截取.更新.删除等操作.数 ...
- 图数据库(graph database)资料收集和解析 - daily
Motivation 图数据库中的高科技和高安全性中引用了一个关于图数据库(graph database)的应用前景的乐观估计: 预计到2017年,图数据库产业在数据库市场的份额将从2个百分点增长到2 ...
- SQL Server中模式(schema)、数据库(database)、表(table)、用户(user)之间的关系
数据库的初学者往往会对关系型数据库模式(schema).数据库(database).表(table).用户(user)之间感到迷惘,总感觉他们的关系千丝万缕,但又不知道他们的联系和区别在哪里,对一些问 ...
- 【转】.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 ...
- Hibernate和jsp做数据库单表的增删改查
虽然很基础,但是我转牛角尖了~~~~这是几个文件 1.最重要的.Java文件 package com.chinasofti.hibb.struts; import org.hibernate.Sess ...
随机推荐
- SQL生成一年每一天的时间列表的几种方法
工作好几年了,一直没有写博客,准备捡起来... 以下脚本适用环境:SQL SERVER (starting with 2012) 1.构建序列: /*1-1:利用交叉连接,推荐下列这种写法 ...
- CSS3 & SVG 制作钟表
在线演示 源码下载
- 通过 jdbc 分析数据库中的表结构和主键外键
文章转自:http://ivan4126.blog.163.com/blog/static/20949109220137753214811/ 在某项目中用到了 hibernate ,大家都知道 hib ...
- SVD之最小二乘【推导与证明】
0.SLAM中SVD进行最小二乘的应用 在SLAM应用中,计算Homography Matrix,Fundamental Matrix,以及做三角化(Triangulation)时,都会用到最小二乘 ...
- mongodb的简明使用
①.特性 文档数据库 高性能高可用性集群 文档是BSON对象 一个collection是一组相关的document,它们共享相同的indexs ②.如何使用 mongo; //进入mongodb ...
- bootstrap+masonry.js写瀑布流
最近在用bootstrap写一个网站,其中有个图文展示的页面要用到瀑布流的效果.因为项目要求,项目要以bootstrap为基准,不准私自添加内联样式.内部样式,所以,自己写瀑布流就不行了,所以,根据要 ...
- java 对时间(Date)随笔!
/** * 获取系统当前时间 * @return 系统当前时间 */ public static Date now() { return new Date(); } /** * 根据指定的日期,获取其 ...
- 小米红米1 android 4.4.4上操作数据库异常问题
产生的问题: 小米红米1 android 4.4.4上,按HOME键,应用进入后台,再启动,应用进程直接挂掉 解决的方法: 这个是操作数据库,数据库关闭之后导致的异常,解决的方法: //4.0以上的版 ...
- java泛型探索——介绍篇
1. 泛型出现前后代码对比 先来看看泛型出现前,代码是这么写的: List words = new ArrayList(); words.add("Hello "); words. ...
- poj1151 Atlanis 线段树+离散化求矩形面积的并
题目链接:http://poj.org/problem?id=1151 很经典的题目,网上有很多模板代码,自己理解了一天,然后很容易就敲出来了... 代码: #include<iostream& ...