单元测试基础知识
本文是阅读了《单元测试之道》一书后的笔记,也是公司安排本人进行单元测试培训的材料,原文是一个Powerpoint,故修改了下,并针对Visual studio 2005自带的单元测试做的一个整理,将其奉献出来,目的是供需要了解和学习单元测试的朋友们阅读。如有错误望指出。
什么是单元测试?
单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
执行单元测试,是为了证明某段代码的行为确实和开发者所期望的一致。
为什么需要单元测试?
当编写项目的时刻,如果我们假设底层的代码是正确无误的,那么先是高层代码中使用了底层代码;然后这些高层代码又被更高层的代码所使用,如此往复。当基本的底层代码不再可靠时,那么必需的改动就无法只局限在底层。虽然你可以修正底层的问题,但是这些对底层代码的修改必然会影响到高层代码。于是,一个对底层代码的修正,可能会导致对几乎所有代码的一连串改动,从而使修改越来越多,也越来越复杂。从而使整个项目也以失败告终。
而单元测试的核心内涵:这个简单有效的技术就是为了令代码变得更加完美。
什么是断言
Assertion(断言),它是一个简单的方法调用,用于判断某个语句是否为真。
例如:
public void IsTrue(bool condtion){
if(!condition) abort();
}
应用则为:
int a=2;
IsTrue(a==2);
还可以编写更多的特定数据类型的断言。
计划你的单元测试
当我们编写了一个如下的函数,它用于查找list中的最大值:static int Largest(int[] list);
所能想到的测试如下:
输入 |
预期结果 |
7,8,9 |
9 |
8,9,7 |
9 |
9,7,8 |
9 |
7,9,8,9 |
9 |
1 |
1 |
-9,-8,-7 |
-7 |
null |
Exception |
创建单元测试
在解决方案资源管理器中右击某个测试项目,或在 Visual Studio 代码编辑器中,右击要测试的命名空间、类或方法并选择“创建单元测试”。
VsUnit 的各种断言
Assert
在测试方法中,可以调用任意数量的 Assert 类方法,如 Assert.AreEqual()。Assert 类有很多方法可供选择,其中许多方法具有若干重载。
CollectionAssert
使用 CollectionAssert 类可比较对象集合,也可验证一个或多个集合的状态。
StringAssert
使用 StringAssert 类可对字符串进行比较。此类包含各种有用的方法,如 StringAssert.Contains、StringAssert.Matches 和 StringAssert.StartsWith。
AssertFailedException
只要测试失败,就会引发 AssertFailedException 异常。如果测试超时,引发意外的异常,或包含生成了 Failed 结果的 Assert 语句,则该测试失败。
AssertInconclusiveException (无结果的)
只要测试生成的结果为 Inconclusive,就会引发 AssertInconclusiveException。通常,向仍在处理的测试添加 Assert.Inconclusive 语句可指示该测试尚未准备好,不能运行。
UnitTestAssertException
编写新的 Assert 异常类时使该类从基类 UnitTestAssertException 进行继承,可更方便地将异常标识为断言失败而非从测试或产品代码引发的意外异常。
ExpectedExceptionAttribute
如果希望开发代码中的某方法引发异常,又想用测试方法来验证是否真的在该方法中引发了异常,则请用 ExpectedExceptionAttribute 属性来修饰测试方法。
如:
[TestMethod]
[ExpectedException(typeof(ArgumentException),
"userID 为 NULL 的异常检测.")]
public void NullUserIdInConstructor()
{
LogonInfo logonInfo = new LogonInfo(null, "P@ss0word");
}
单元测试的属性
除了单元测试方法的 [TestMethod()] 属性及其包容类的 [TestClass()] 属性之外,可使用其他属性启用特定的单元测试功能。在这些属性中,最主要的属性有 [TestInitialize()] 和 [TestCleanup()]。使用标记有 [TestInitialize()] 的方法对将要在其中运行单元测试的环境的各个方面进行准备;这样做的目的在于为单元测试的运行建立已知的状态。例如,可以使用 [TestInitialize()] 方法复制、更改或创建测试中将要使用的某些数据文件。
在运行完某个测试后,可通过标记有 [TestCleanup()] 的方法将环境返回到已知状态;这可能意味着需要删除文件夹中的文件,或将某个数据库返回到已知状态。例如,在测试了订单录入应用程序中使用的某个方法后,可将库存数据库重置为初始状态。此外,建议您在 [TestCleanup()] 或 ClassCleanup 方法中使用清除代码,而不要在终结器方法(~Constructor)中使用此代码。从终结器方法引发的异常不会被捕捉到,并且会导致无法预料的结果。
用于建立调用顺序的属性
对于程序集:
在加载程序集之后以及卸载程序集之前,将调用 AssemblyInitialize和 AssemblyCleanup。
对于类 :
在加载类之后以及卸载类之前,将调用 ClassInitialize 和 ClassCleanup。
对于测试方法 :
在每个测试方法加载以及卸载之前,将调用TestInitialize 和TestCleanup
什么是VsUnit 的TestContext 类
测试上下文类的属性存储有关当前测试运行的信息。例如,TestContext.DataRow 和 TestContext.DataConnection 属性包含测试用于数据驱动的单元测试的信息。
其他
用于对测试进行标识和排序的属性
测试配置类
用于生成报告的属性
用于专用访问器的类
测试哪些内容:Right-BICEP
Right----------结果是否正确?
B---------------是否所有的边界条件都是正确的?
I----------------能查一下反向关联吗?
C---------------能用其他手段交叉检查一下结果吗?
E---------------你是否可以强制错误条件发生?
P---------------是否满足性能要求?
RIGHT:结果是否正确
如果代码能运行正确,如何才知道它是正确的呢?
1、使用更明确的设计文档
2、真实环境数据
3、????
B:边界条件
尽可能的至少各种特殊或者意外的情况,测试程序是否能正常工作,如:
l 完全伪造或者不一致的输入数据,如叫做“(*@Q!&#?±的文件。
l 格式错误的数据,如错误格式的邮件地址
l 空值或不完整的值
l 一些与意料中的合理值相去甚远的值,如年纪为10000
l 如果要求是一个不允许出现重复数值的list,但传入一个有重复数值的list
l 要求是一个有序的list,但传入一个无序的list
l 处理的顺序是错误的,或者与期望的次序不一致。如未登录系统就尝试打印。
B:边界条件的-CORRECT
Conformance(一致性)值是否和预期的一致
Ordering(顺序性)值是否应该的那样有序或者无序
Range(区间性)值是否位于合理范围
Reference(依赖性)代码是否引用了一些代码本身控制范围之外的资源
Existence(存在性)值是否存在(是否非空,非零,在集合中等等)
Cardinality(基数性)是否恰好有足够的值
Time(相对或绝对的时间性)所有事情的发生是否有序?是否在正确的时间?是否恰好及时?
I:检查反向关联
通常一些结果可以使用反向的逻辑关系来验证它们是否正确,如:计算a*b的函数,测试方法如下:
Public void UsingInverse(){
double x = MyMath.AB(4,4);
Assert.AreEqual<double>(x,4*4);
}
C:使用其他手段实现交叉检查
计算一个结果可以存在多个算法,同一个算法可以使用稳定的版本来校验新改进的版本,如:
Public void UsingStd(){
double number = 23214.01;
double result1 = MyMath.旧方法(number1);
double result2 = MyMath.新方法(number1);
Assert.AreEqual<double>(result1,result2);
}
E:强制产生错误条件
真实运行环境各种出乎意料的事情都可能发生,如断电,断网等等。在测试中模拟这些情况可以使用Mock对象来实现。
P:性能特性
如数据采集的功能,在十个网站上进行采集工作很正常,那么在1000个网站上或更多的网站上进行采集它的速度如何?是否写个单元测试?
如何才是一个好的单元测试
好的测试应该具有的品质是:A-TRIP(合称)
Automatic 自动化
Thorough 彻底的
Repeatable可重复
Independent独立的
Professional 专业的
Automatic自动化
调用自动化
检查结果自动化
Thorough 彻底的
测试所有可能会出现问题的情况,一个极端是,对于每行代码、代码可能到达的分支,每个可能抛出的异常等等,都可以作为测试的对象。另一个极端是,你仅仅测试最可能的情况---边界条件、残缺和畸形的数据等等。然而这些都基于项目需求的决策问题。
这些所说的归纳为“代码覆盖率”。
Repeatable 可重复
测试应该独立于所有其他的测试,而且必须独立于周围的环境。目标只有一个,就是测试应该能够以任意的顺序一次又一次的运行,并且产生相同的结果。如果结果不同,则存在BUG。
Independent 独立的
编写测试时,确保一次只测试了一样东西,但并不表示一个TestMethod内只能使用一个Assert,而是一个测试函数应该专注于产品代码中的一个函数,或者组合起来并共同提供某个特性的一组函数。
Professional 专业的
测试代码必须同产品代码相同的风格来编写。这意味着你需要抽取出共同且重复的代码,并把它们放到一个功能类之中,从而可以复用;单元测试的代码一样讲究------维护封装,DRY原则,降低耦合。
何时需要Mock对象
l 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
l 真实
Mock对象的三个步骤
对象很难被创建(比如具体的web容器)
l 真实对象的某些行为很难触发(比如网络错误)
l 真实情况令程序的运行速度很慢
l 真实对象有用户界面
l 测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
l 真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)
Mock对象的三个步骤
1. 原型
ClassA调用 ClassB的Method()
2. 使用一个接口来描述这个对象
ClassA 通过接口调用ClassB的Method
3. 以测试为目的,在mock对象中实现这个接口