谈一下我们是怎么做数据库单元测试(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…

ssl2293-暗黑游戏【dp练习题】

题目: 暗黑游戏中,装备直接决定玩家人物的能力。可以使用Pg和Rune购买需要的物品。暗黑市场中的装备,每件有不同的价格(Pg和Rune)、能力值、最大可购买件数。Kid作为暗黑战网的一个玩家,当然希望使用尽可能…

java知识点整理

1.char类型可以整型类型的值 2.变量的命名规则: (1)由字母、、$ 开头的 (2)后面部分可以拾字母、数字、下划线、$ (3)不能是java的关键词 (4)变量名要有意义 3.0是偶数 4.数据类型转换规则: int(源类型) a 1.0(目标类型); (1)源类型大于目标类型&#…

比特(bit)和字节(byte)(1byte=8bit)

一个0或者一个1存储为一个比特(bit),是计算机中最小的存储单位。 计算机中是最基本的存储单元是字节(byte) 。每个字节由8个比特构成。

定位

绝对定位 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title></title><style>.div1{height: 200px;width: 200px;background-color: gray;/*绝对定位 基于父级标签原点移开以后会自动释放父级标签原点位置*/po…

Web前端知识体系精简

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

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

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

《金色梦乡》金句摘抄(十一)

System.out.println("《金色梦乡》"); System.out.println("小说类型的书就是比散文类型的书好看"); System.out.println("通俗易懂"); System.out.println("今天准备看完");从一开始他就觉得难以置信&#xff0c;感觉就像是身处迷雾当…

ssl2295-暗黑破坏神【dp练习】

题目&#xff1a; 无聊中的小x玩起了Diablo I... 游戏的主人公有n个魔法 每个魔法分为若干个等级&#xff0c;第i个魔法有p[i]个等级(不包括0) 每个魔法的每个等级都有一个效果值&#xff0c;一个j级的i种魔法的效果值为w[i][j] 魔法升一级需要一本相应的魔法书 购买魔法书…

内存VS硬盘

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

依赖注入之Autofac使用总结

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

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

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

《走遍中国》珍藏版(一)

Console.WriteLine("上一本书已经看完&#xff0c;从今天开始看下一本"); System.out.println("《走遍中国》珍藏版"); System.out.println("这本书是关于地理的"); System.out.println("地理往往是和历史进行紧密联系的");北京、天津…

MySQL建表,DML,DDL,约束,外键策略

创建数据库表 CREATE TABLE student( sno int (6), sname VARCHAR(10), sex CHAR(1), age INT(2), enterdate date, classname VARCHAR(10), email VARCHAR(15) ); – 查看表的结构 desc student – 查看表的数据 SELECT * FROM student DML – 查询表的数据 SELECT * FRO…

ssl1197-质数和分解【dp练习】

Description   任何大于 1 的自然数 n&#xff0c;都可以写成若干个大于等于 2 &#xff0c;且小于等于 n 的质数之和表达式(包括只有一个数构成的和表达式的情况)&#xff0c;并且可能有不止一种质数和的形式。例如9 的质数和表达式就有四种本质不同的形式&#xff1a; 9 …

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

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

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

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

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

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

《走遍中国》珍藏版(二)

Console.WriteLine("上一本书已经看完&#xff0c;从今天开始看下一本"); System.out.println("《走遍中国》珍藏版"); System.out.println("这本书是关于地理的"); System.out.println("地理往往是和历史进行紧密联系的");大门两侧傲…

单表查询

最简单的查询语句 select * from dept select * from emp; – 显示部分列 select empno,ename,sal ,comm,deptno from emp – 显示部分行 where select empno,ename,sal ,comm,deptno from emp where sal<2500 – 别名 select empno 编号,ename 姓名,sal 工资 ,comm 补助…