《.NET单元测试的艺术》读书笔记

来源:互联网 发布:ntfs for mac价格 编辑:程序博客网 时间:2024/04/28 12:38




单元测试基础知识 => NUnit框架 =>伪对象(桩,模拟对象)=>隔离框架(Rhino Mocks =>测试的层次组织 =>优秀的单元测试定义(可信赖,可维护,可读)=>测试的实施经验



基础知识

基本概念
单元测试定义:一段自动化的代码调用另一段代码,随后验证一些假设的正确性。单元测试几乎总是用单元测试框架来写的。它是全自动的,可信赖的,可读性强的,可维护的。
集成测试定义:把两个或者多个相依赖的软件模块作为一组进行测试。
单元测试和集成测试区别:
单元测试测的是一个单元,而集成测试测得是多个集成到一起的单元。
“回归”定义:以前好用的功能,现在却出现了问题。
TDD: 测试驱动开发。测试代码写在生产代码之前。
NUnit-单元测试框架;Rhino Mocks-创建桩对象和模拟对象的隔离框架。
NUnit基本知识
NUnit-.Net,免费开源; CppUnit-C++;JUnit-Java
单元测试命名规则:
测试项目 – [被测试项目].Tests 
测试类 – [被测类名]Tests
测试方法 – [被测方法名]_[测试场景]_[预期行为]。比如:IsValidFileName_validFile_ReturnsTrue().
NUnit中的特性:
[TestFixture]-用于标识NUnit的自动化测试类。
[Test]-标识NUnit的测试方法,表示一个需要被调用的自动化测试方法。
[SetUp]-应用于方法,NUnit在运行测试类中的每个测试(方法)之前都会执行SetUp方法。
[TearDown]-应用于方法,会在测试类中每个测试(方法)运行结束后,执行一次。
 
[TestFixtureSetup]-在一个指定测试类中的所有测试运行之前调用。
[TestFixtureTearDown]-在一个指定测试类中的所有测试运行结束后调用。
[ExpectedException]-用于测试异常。验证是否抛出。
[Ignore]-用于[Test]标记的测试方法上,忽略这个测试。在NUnit中显示为黄色。
[Category]-用于设定测试级别。完后在NUnit中选择运行特定的类别。
NUnit测试方法的返回类型必须是void,且参数为空。
NUnit框架中的Assert类中定义了一些断言静态方法。
比如Assert.IsTrue(),Assert.AreEqual()-期望的对象是否与实际的一样,Assert.AreSame()-两个参数引用的是否是同一个对象。
NUnit会自动检测测试程序集改变并自动重新加载。
使用NUnit步骤:
1. 创建测试项目[被测项目].Tests
2. 在类库中添加对被测项目的引用。
3. 添加NUnit.Framework程序集的引用。
4. 添加测试类[TestFixture]和测试方法[Test].
5. 在测试方法中调用被测对象,并使用Assert断言。(另外可加SetUp,TearDown等特性)。
6. 在NUnit图形界面中添加该测试程序集,开始测试。



核心技术


桩对象(Stub)
桩对象定义:系统中现有依赖项的一个替代品,可人为控制。通过使用桩对象,无需涉及依赖项,即可直接对代码进行测试。
接缝:指代码中可以插入不同功能(如桩对象类)的地方。
使用桩思想:a.找到被测对象对应的接口;b.将接口底层实现替换成自己可控的东西(策略模式)。
 

桩实现步骤:
a. 抽取接口。即桩和依赖对象都派生于该接口。
b. 被测试代码中使用接口。(桩是替换被测代码中的依赖项)。
c. 注入桩。即把桩对象传入被测试代码。
注入桩的三种方法(就是把抽取接口替换成桩实例):
a. 构造函数注入。
b. 使用抽取的接口类型的属性。
c. 在使用测试代码前通过工厂方法,工厂类等方式接收一个接口。
构造注入:
即在被测类的构造里面把抽取接口替换成桩实例。
 
如果增加一个桩,则需要增加一个构造函数,这是缺点,优点是表明注入是必选的必须指定的。
如果有多个桩,则可以把桩接口封装到类中,构造函数只传类,但这种做法可能会导致一个类里面有几十个属性。
属性注入:
即定义一个public属性来接收桩实例。
 

工厂类实现桩注入:
即在被测代码里面使用工厂类的静态方法返回的桩实例赋给抽取接口。在工厂类里如果是测试模式则返回桩实例,如果是发布模式则返回实际的依赖对象。
 

工厂方法实现桩注入:
即在被测试类中定义一个返回真实依赖对象的虚的工厂方法,而测试类派生于被测试类,重写该工厂方法,这个挺麻烦,参见P72.
 

使用internal和[InternalsVisibleTo]来使得测试代码仅对程序集可见。
使用[Conditional]和#if来使得测试代码不被包含到发布程序里。
模拟对象(Mock)
基于状态的测试:指在方法执行之后,通过检查被测系统及其协作者(依赖项)的状态来检测该方法是否正确工作。---结果驱动测试 --桩
交互测试:用来测试一个对象如何与其他对象进行交互。---动作驱动测试 –模拟对象
模拟对象定义:通过验证被测对象和模拟对象之间是否进行预期的交互来决定测试是通过还是失败。测试的是交互。通常每个测试只有一个伪对象。
桩对象和模拟对象的区别:

 
桩对象:断言针对被测类。桩对象不会使测试失败。一个测试项目内一般可以有多个桩对象。

 
模拟对象:断言针对模拟对象。模拟对象会使测试失败。一般一个测试项目只有一个模拟对象。
伪对象(fake object):是桩对象或者模拟对象的概称。
一个测试最好有且只有一个模拟对象,剩下的都是桩对象。一个测试中如果有多个模拟对象则意味着你在测多件事,这会导致测试变得复杂或脆弱。
隔离框架
隔离框架定义:创建模拟对象和桩对象的一组API框架。C++有Mockcpp, Java有jMock,。NET有NMock和Rhino Mocks.
Rhino Mocks:是一款免费的开源隔离框架。通过类MockRespository的方法来创建桩对象和模拟对象。有两种机制:录像-回放机制和设置-操作-断言机制。
Rhino Mocks创建动态模拟对象的一般步骤:
1. New一个MockRepository对象,调用其比如StrictMock()方法来创建运行时模拟对象。
2. 设置预期。
3. 注入模拟对象,调用测试代码。
4. 使用MockRepository的Verify或者VerifyAll方法来验证预期。
 


严格模拟对象:
只能被明确定义的预期方法所调用。也就是只能按照设置预期那样调用,任何参数,方法名不同,都会抛出异常。通过MockRepository.StrickMock<T>()来创建。
非严格模拟对象:
允许针对模拟对象的任何调用。仅当预期方法没有调用时才会导致测试失败,前提是要调用Verify,否则测试会通过。通过MockRepository.DynamicMock<T>()来创建。
从模拟对象返回值通过类LastCall.Return()来实现(最好不要这么用,用桩)。
模拟对象根据预先设置的输入值来返回结果。如果设置同样的输入,但设定的返回结果不同,则根据在代码中设计的顺序来返回。参见p112.
 

MockRepository.Verify和VerifyAll只适合模拟对象不适用桩对象,因为桩对象不会让测试失败。
Rhino Mocks创建桩对象:
MockRepository.GenerateStub<T>()
MockRepository.Stub<T>()
仍然使用LastCall.Return()为桩对象方法返回值。
 
 


但不能用Verify, VerifyAll.
Rhino Mocks中对桩对象和模拟对象的参数约束
即对桩对象和模拟对象中的函数参数进行约束。
1.使用LastCall.Contraints()方法+约束类(p121)来给桩对象和模拟对象添加参数约束:
 
2. LastCall.CallBack(回调函数)来使用回调函数检查参数。
Rhino Mocks模拟对象和桩对象触发事件:
 
测试一个事件是否被触发:
 


 

如果使用手写模拟对象就能满足需求,就不要使用隔离框架。




测试层次及组织



测试层次
集成测试和单元测试要分开
映射测试到类两种方式:
一个测试类对应一个被测类。
一个测试类对应一个功能。
测试类的继承模式:
1. 抽象测试架构模式:即把测试类共用的方法Setup和TearDown放到一个测试基类中:
 


2. 测试模板类模式:即建一个抽象的测试基类,包含派生测试类必须实现的抽象方法:
 
3. 抽象测试驱动类模式:新建一个抽象类,实现所有的测试方法,派生类只需默认继承这些方法,而不需要再次实现他们。
 
优秀的单元测试
优秀的单元测试要具有三个属性:可信赖性,可维护性,可读性
可信赖性:
1. 测试中不该有太多逻辑。
2. 只测一件事,不要有多个断言。如果有多个事要测,则拆成多个测试。实在要在一个测试中要测多个,则使用[RowTest]特性。
[RowTest]--参数化测试,一个[RowTest]失败,其他的仍会运行。而断言一个失败则后续的不会运行。P210,211.
3. 易运行,确保覆盖率。
可维护性:
1. 去除重复代码:工厂方法,Setup等。
2. 实施测试隔离:
a. NUnit不保证测试按照指定顺序执行
b. 不要在一个测试中调用另一个测试
c. 测试之间尽量不要共享状态,用单独的实例。
可读性:
1. 单元测试命名规则。参见前章节。
2. 尽量避免将断言和方法放在同一个语句中。
即不要在断言里有函数调用



测试实施(经验)


自下而上实施变革:
先从一个团队开始,获得一些成果,然后用证据去说服别人这些实践是有价值的。
自上而下实施变革:
说服管理层,从领导层开始实施,然后在组织里的其余部分逐步实施。
把代码覆盖率作为目标。
修改遗留的代码:
a. 从哪里开始测试:根据逻辑复杂度,依赖等级和优先级三个指标创建测试可行性表,进而画出示意图。
b. 根据示意图确定从哪开始测试:
从容易测试入手 --- 团队单元测试经验不住足。
从困难测试入手 --- 团队测试经验丰富。

也可以参考p258修改遗留代码工具。