.net程序单元测试介绍
什么是单元测试?为什么要进行单元测试?如需要进一步了解,请移步维基百科。
关于.net程序单元测试的文章,网上已经有很多,但我相信我写的这篇文章的内容是独特的,因为我在网上找了很久,都没找到关于StructureMap(参考前一篇博文《谈谈.net模块依赖关系及程序结构》)与VisualStudio单元测试相结合的好教程。但我必须声明一下,这只是我的方法,并不代表就是最好的方法,也许你能找到更好的。
.net单元测试的简单例子
照旧,从一个最简单的例子开始,这里没有StructureMap,也没有模块与模块之间的依赖关系,只有一个简单的模块——HR,需要进行单元测试,我们来感受下,究竟什么是单元测试。先看下程序结构图:

只有一个Hr模块,暴露了一个IHr接口,我们现在写一个叫“TestDemo”的程序,来对它们进行单元测试。测试代码片段如下:
[TestClass]
public class HrTester
{
[TestMethod]
public void TestGetAllEmployees()
{
//...
} //... [TestMethod]
public void IntegrationTest()
{
IHr hr = new HrManager(); const string empNoToTest = ""; try
{
hr.AddEmployee(new Employee_Add() { EmpNo = "", Name = "Somebody", Birthday = DateTime.Now });
Assert.Fail("尝试增加已存在工号应当抛出异常");
}
catch (Exception ex)
{
Assert.IsInstanceOfType(ex, typeof(DemoException));
} //...
}
}
代码太多,我只贴了一小部分出来说明,我是定义了一个叫HrTester的类来对Hr模块进行单元测试的,这个类被加上“TestClassAttribute”修饰,而类中需要被测试的方法则加上“TestMethodAttribute”修饰,这两个修饰类都来自“Microsoft.VisualStudio.QualityTools.UnitTestFramework”,需要给测试工程引用这个类库。

当如上图般启动了单元测试之后,VisualStudio就尝试在项目中寻找有“TestClass”修饰的类,并且从这些类中寻找有“TestMethod”修饰的方法,执行这些方法,当然了,也可以直接指定某个测试类或者某个测试方法来让VisualStudio进行单独的测试,而不是全部。如果一个测试方法跑下来没有发生任何异常,也没有任何断言被触发,那么就被认为测试通过,比如你定义了一个空的测试方法,那么毫无疑问是能通过的。另外需要额外说明的一点是:测试方法的定义有些要求,不能带参数,必须为public,返回类型必须是void,想想确实应该如此啊……测试结果会展示在TestExplorer窗口中:

(可通过 <TEST> - <Windows> - <Test Explorer>打开此窗口)
如果测试不通过,那显示出来的就不是绿色的小勾,而是个红色的小叉,点上去,可以查看详情。
从单元测试设计上来说,可以给IHr接口的每个方法都写一个测试方法,也可以统统合并为一个“综合测试方法”,如我上面的“IntegrationTest”就是一个综合方法,我偏向于使用综合方法,一来可以减少一些编码量,二来是业务逻辑上的需要,比如我希望在新增完一个员工之后,再马上将此员工的信息取出作比较,以此来判断正确与否,实践中究竟怎么写,这个要根据实际情况。
为了能够方便进行全方面的测试,我们在做接口设计的时候也要把测试考虑进去,比如要写完整的“获取”方法,方便执行了各种操作之后,能够重新取回值进行验证检查。
关于Assert的其它使用说明,可以参考MSDN。
当然了,也许你看到这里还是有些不太明白,如果之前没有做过单元测试的话,没关系,我提供了完整代码,操练下就很快明白了。
DEMO1完整代码(VS2012)
面向接口编程的优势
这里的“面向接口编程”并非是区别于“面向对象编程”的另外一种编程思想,它其实只是属于面向对象编程中的一种设计模式,即只要固定好了接口,那么接口实现者的改变就能对接口调用者透明,反之依然。其根本目的是什么?——解耦!
现在,我们对我们的DEMO1程序进行一些调整,修改HR模块的实现方式,这次使用SQL Server 2012 Express,而不是一个内存对象来存储数据。
这里顺便提一下SQL Server 2012 Express的使用方式,一开始我还有些被搞糊涂了,明明安装了,但在系统服务中就是看不到相关的服务的存在,原来跟SQL Server 2008 Express相比,它的机制变了,用微软的话说,是更加节省资源,只有你在使用它的时候,服务才会启动起来,平常是看不到的。我们使用SQL Server Management Studio去连接的时候,要填的东西跟之前的也是有些区别的,如图:

服务器名称这栏变成了“(localdb)\v11.0”,而不是之前的“.\SQLEXPRESS”。你可以通过命令行查看目前有哪些数据库运行实例:
>sqllocaldb info
用这个命令还可以创建和删除实例,以及查看实例的详情,具体使用可以“sqllocaldb -?”。默认情况下,可以在“%userprofile%\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances”目录下找到数据库实例的数据文件。OK,我觉得应该就此打住,毕竟本文不是讲数据库的。大家还是直接下载我的DEMO2来看一下吧,这算是一段附加说明,跟单元测试关系不大,我只是想说明这么一个观点:面向接口很有爱。
这里附上数据库创建的相关脚本(先创建一个叫“hr_db”的数据库):
create table employees(
emp_no nvarchar(20) primary key,
name nvarchar(20) not null,
birthday datetime not null,
descriptions nvarchar(200),
status nvarchar(15) not null,
salary decimal(10,2) not null
); insert into employees(emp_no, name, birthday, descriptions, status, salary) values
(N'', N'蒋国纲', '1981-11-12', N'技术宅男', N'Normal', 5000.00); insert into employees(emp_no, name, birthday, descriptions, status, salary) values
(N'', N'周星驰', '1962-06-22', N'喜剧之王', N'Probation', 2000.00); insert into employees(emp_no, name, birthday, descriptions, status, salary) values
(N'', N'李逍遥', '1958-12-19', N'蜀山派掌门人', N'Leaved', 8000.00);
DEMO2完整代码(VS2012)
StructureMap与单元测试
上面的例子是否太简单了?不过话说不管怎么简单的东西,你还是得真正动手捣鼓下才能弄明白。
现在,我们来玩稍微复杂一点的,这次加入了一个Log模块,HR模块依赖于它,用StructureMap(如对StructureMap不了解,请查看本文开头提到的“前一篇博文”)来管理它们的依赖关系,程序结构图变成了这样:

HR模块,需要使用Log,它是通过这种方式来请求到ILog的:
ILog logger = ObjectFactory.GetInstance<ILog>();
而程序启动的时候,我们会配置好StructureMap,来指明“ILog”及其实现类之间的关系:
ObjectFactory.Initialize(x =>
{
x.For<ILog>().Singleton().Use<LogManager>().Ctor<string>("logPath").Is(AppDomain.CurrentDomain.BaseDirectory + "log");
});
OK,聪明的读者,有没有发现什么问题?
注意!我们是在“程序启动的时候”来配置StructureMap的,但运行测试项目的时候并不会启动程序,那我们应该在什么地方来做这个配置?
所幸的是微软也考虑到了这点,所以微软提供了AssemblyInitializeAttribute来供我们使用,告诉测试环境,这个方法是在启动测试前要执行的。这是完整的写法:
[TestClass]
public static class ContainerBootstrapper
{
[AssemblyInitialize]
public static void BootstrapStructureMap(TestContext context=null)
{
ObjectFactory.Initialize(x =>
{
x.For<IHr>().Singleton().Use<HrManager>();
x.For<ILog>().Singleton().Use<LogManager>().Ctor<string>("logPath").Is(AppDomain.CurrentDomain.BaseDirectory + "log");
});
}
}
class在这里也必须加上“TestClass”修饰,而修饰为“AssemblyInitialize”的方法必须为public、static、void、以及需要带上一个TestContext类型的参数。关于AssemblyInitialize的完整帮助,可以查阅MSDN。
其余的都跟DEMO2没什么差别,测试程序我这里是一点都没变。
好,自行演练时间到:DEMO3完整代码(VS2012)
Mock——真正的单元测试
大家停下来想想DEMO3有什么问题?
问题就是HR模块依赖于Log模块,要对HR模块进行单元测试,就必须先实现好Log模块,而且要求Log模块没有问题,否则单元测试就可能通不过,从这个角度看,这不是真正的对HR模块的单元测试,而是对多个模块的综合测试了……假如HR模块以后还依赖于Office模块、Flow模块、Administration模块……更糟糕的是Office模块又依赖于Vehicle模块、Finance模块、PubMaterial模块……那就意味着这个“单元测试”几乎没法做了,因为这些模块可能是分配给不通的人去实现的,有些人写完了,有些人没写完,只要有一个模块没写完,就没办法对HR模块进行单元测试,只要一个依赖模块有bug,就可能导致HR模块的单元测试通不过。如何解决?
最直截了当的方法当然是:我们来造一个假的Log模块,完全实现ILog接口,但实际上可能并不会真正地记录日志。这样就可以让单元测试进行下去。
这种造假的方法就叫“Mock”,Mock这个单词和Imitate是同义词,仿造,模仿的意思。现在我们来Mock一个Log模块:
class Mock_ILog : ILog
{
public void Log(LogType logType, string moduleName, string content, params object[] values)
{
Debug.Write("ILog::Log is called.");
}
}
太简单了,这个类实现了ILog接口,但它显然没记录日志,只是打印一些debug信息。即便实现Log模块的那位兄弟啥都没写,我们的单元测试也可以开始了。
不过别高兴得太早,到了这步,我们还是碰到了一些问题,必须得先解决:StructureMap的配置是否得改?按理说是不能改的,因为那是正常程序运行所需要的,你做单元测试的时候希望使用“Mock_ILog”,而真实的运行还是得用真家伙啊,如果每次做单元测试的时候都要先改一通,测完之后再改回来,那是不是很繁琐?而且一不小心就改错?
所幸的是:微软和StructureMap的作者都想到了这点。我在HrTester类中加入以下的代码:
[ClassInitialize]
static public void Init(TestContext context)
{
ObjectFactory.Configure(x => { x.For<ILog>().Singleton().Use<Mock_ILog>(); });
} [ClassCleanup]
static public void EndTest()
{
ObjectFactory.ResetDefaults();
}
ClassInitialzie修饰表示在测试此类前,此方法会被调用一次;而ClassCleanup修饰则表示测试完此类后,此方法会被调用一次。ClassInitialzie的具体说明:MSDN;ClassCleanup的具体说明:MSDN。
ObjectFactory.Configure方法可以动态修改StructureMap的配置,而ObjectFactory.ResetDefaults方法则可将StructureMap的配置还原为初始状态。这样一来,测试的归测试,正式的归正式,互不干扰。
使用Moq简化Mock
如果依赖关系复杂起来了,那给对每个模块都Mock一下就让人感觉有些繁琐,(其实通过上面的那个ILog的例子看也繁琐不去哪里)有没有自动帮创建Mock的工具?——当然有,其中一个是Moq,Moq的最简单用法如此般:
Mock<ILog> mockILog = new Mock<ILog>();
ILog logger = mockILog.Object;
这样一个Log模块就mock出来了,当然了,你尝试调用它的Log方法的话,你很快就发觉这是一个空方法。
那我想跟上面那个手工创建的Mock_ILog那样,能打印点Debug信息出来怎么办?写法稍微有点复杂:
Mock<ILog> mockILog = new Mock<ILog>();
mockILog.Setup(c => c.Log(It.IsAny<LogType>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Object[]>()))
.Callback<LogType, string, string, Object[]>((a, b, c, d) => Debug.WriteLine("ILog::Log is called."));
ILog logger = mockILog.Object;
从这段代码看,Moq有时候恐怕也方便不去哪里,记住!它只是提供了一种可选的方法,它本身并不是解决单元测试问题的银弹。限于篇幅,我不可能在这里完整讲解Moq究竟怎么用,大家可以自行google,或者到StackOverflow.com去找,可能更快能找到答案。
当我们用Moq来帮我们生成了这么一个“假对象”了之后,要把这个假对象告知StructureMap,让StructureMap在测试的工程中给ILog的请求者返回这个假对象。
ObjectFactory.Inject(typeof(ILog), mockILog.Object);
最后特别说明一下:用这种方法“注入”到ObjectFactory中去的对象都是单实例的,对于一个Mock对象来说,单实例没什么问题,我们只是为了单元测试。
在最后提供的这个DEMO4的代码中,我还额外增加了一个IOffice接口,但没有具体的实现,而HR模块依赖之,所以在对HR进行单元测试过程中,就是用Moq来做了一个假对象来“混过去”的。
DEMO4完整代码(VS2012)
快过年了,祝大家新年快乐!
.net程序单元测试介绍的更多相关文章
- Java程序单元测试工具对比——Parasoft Jtest与Junit
Web应用程序开发中,面向对象的Java语言占了不少的比重.对于Java应用程序的测试方法或方式多种多样,比较典型的是程序员自己来完成程序测试中的一个部分——单元测试. 之前,慧都资讯提到单元测试是程 ...
- 【6集iCore3_ADP触摸屏驱动讲解视频】6-1 工程及程序构架介绍
视频简介: 该视频由银杏科技有限公司基于iCore3应用开发平台推出,包含 触摸屏驱动工程文件的介绍与程序构架的介绍等. 源视频包下载地址: http://pan.baidu.com/s/1dFz ...
- 【30集iCore3_ADP出厂源代码(ARM部分)讲解视频】30-2 工程及程序构架介绍
视频简介: 该视频介绍iCore3应用开发平台出厂源代码中ARM部分程序构架. 源视频包下载地址: http://pan.baidu.com/s/1jHGnc7k 银杏科技优酷视频发布区: htt ...
- 【转载】Java中如何写一段内存泄露的程序 & ThreadLocal 介绍和使用
可以参考这段文章: link A1:通过以下步骤可以很容易产生内存泄露(程序代码不能访问到某些对象,但是它们仍然保存在内存中): 上文中提到了使用ThreadLocal造成了内存泄露,但是写的不清不楚 ...
- 微信小程序开发02-小程序基本介绍
前言 前面我们研究了下微信小程序的执行流程,因为拿不到源码,只能算我们的猜想,我们需要更加了解小程序还需要做具体的项目,于是我们将原来那套还算复杂的业务拿出来: [组件化开发]前端进阶篇之如何编写可维 ...
- Android 程序目录介绍
还是回到 Eclipse 中,首先展开 HelloWorld 项目,你会看到如图 1.17 所示的目录结构. 图 1.17 一开始看到这么多陌生的东西,你一定会感到有点头晕吧.别担心,我现在就对上 ...
- 音视频处理之FFmpeg程序的介绍与使用20180302
一.FFMPEG程序介绍与使用 主要介绍一下ffmpeg工程包含的三个exe的使用方法. 1. FFMPEG程序介绍 1.1.下载 ffmpeg的官方网站是:http://ffmpeg.org/ 下载 ...
- 开发Java web应用程序的介绍
本文将介绍使用Eclipse来开发web应用程序的基础知识.它演示了如何创建一个简单的web应用程序,将其部署到服务器,查看其呈现在浏览器中.应用程序使用JavaServer Pages™(JSP)页 ...
- Arduino关于旋转编码器程序的介绍(Reading Rotary Encoders)--by Markdown
介绍 旋转或编码器是一个角度測量装置. 他用作精确測量电机的旋转角度或者用来控制控制轮子(能够无限旋转,而电位器只能旋转到特定位置).其中有一些还安装了一个能够在轴上按的button,就像音乐播放器的 ...
随机推荐
- 不容错过!2016年度优秀UI/UX设计文章
本文整理了一些2016年度最受欢迎的文章,例如有关UI / UX设计的理论知识,书籍和工具,如何做出更好的设计的方法和建议,以及新的设计趋势. 1. 2017年用户体验设计趋势 我们期待着2017年用 ...
- ADV-caikuang
#include<stdio.h> int step[99][99]; int sum; int min=999999; int cas; int N; int H; int V; int ...
- c# Exception 异常信息归整
private string ErrorMessage(Exception exception) { StringBuilder stringBuilder = new StringBuilder() ...
- linux自动以root登录,并自动启动用户程序的设置方法
系统自动以root登录,并自动启动用户程序的设置方法 第一步:删除root用户 vi /etc/passwd 该文件的第一行:root:X:0:0:root:/root:/bin/bash,只需要把第 ...
- How to copy remote computer files quickly to local computer
if we want copy file from VM(Remote VM) to local computer. Always can not easy copy file so easy. no ...
- lua 快速排序
function partion(arr, left, right) local tmp = arr[left] while left < right do while left < ri ...
- 【Visual Lisp】两种出错处理方式
两种出错处理方式:一种是对出错函数进行重定义,一种是对错误进行捕捉处理. ;;============================================================= ...
- linux 匹配字符串是否为数字
#!/bin/bash ## 方法1 a=1234;echo "$a"|[ -n "`sed -n '/^[0-9][0-9]*$/p'`" ] &&a ...
- Codeforces #380 div2 D(729D) Sea Battle
D. Sea Battle time limit per test 1 second memory limit per test 256 megabytes input standard input ...
- js中,var 修饰变量名和不修饰的区别
js中 允许在定义变量的时候 不加var 修饰符.js会在当前作用域下寻找上下文是否定义了此变量, 如果没有找到则会为这个变量分配内存.当且将其视为window的成员. 也就是全局变量. 如果加了va ...