现在接着上次说到的内容,如何在项目中进行单元测试。由于做了一些调整,我上次发的内容,我这次也会重新发一次,请认真看看。
一、软件开发过程中存在的问题(没有使用单元测试的情况下)
- 难于定位bug的位置
- 修改一个bug,容易引进n个bug
- bug越后期发现,修改越困难
- 后期系统的复杂性,导致难于修改和重构代码
- 开发人员常认为编译通过,进行了几次手工测试就等于测试通过
- 在完全依赖外部系统的情况下,无法进行有效的测试
- 手工测试效率地下,针对性不强
- 代码难以维护和复用
- 开发人员觉得测试和修改是额外的工作,认为代码通过编译和调试就完成任务
二、单元测试在开发过程中起到的作用
- 单元测试大大节约了测试和修改的时间
- 单元测试能快速定位bug
- 单元测试能使开发人员重新审视需求和功能的设计
- 单元测试强迫开发者以调用者而不是实现者的角度来设计代码,利于代码之间的解耦
- 自动化的单元测试能保证回归测试的有效执行
- 有效且便于测试各种情况
- 使代码可以放心修改和重构
- 单元测试可用作被测代码的用法说明,可作为开发文档使用
- 测试用例永久保存,支持随时测试
以下举一个本人项目的例子,这个被测方法是验证登陆用户是否为管理员或版主
例子:public bool IsAdminOrBoardMaster(IOnLineUser p_OnLineUser) { if (!IsRegularUser(p_OnLineUser))//判断是否为有效用户 return false; if (p_OnLineUser.UserType == UserType.Teacher)//判断是否为教师 { if (IsHeadmanForUser(p_OnLineUser.UserIdentity))//判断是否为教师组组长 return true; else return IsCourseClassBoardMasterForUser(p_OnLineUser.UserId);//判断是否为课程班带教老师 } else if (p_OnLineUser.UserType == UserType.Student)//判断是否为班长 return p_OnLineUser.UserIdentity == 1;//身份为1的话,用户为班长,返回true else return true;//管理员 }
从以上例子可有以下的测试用例
- 用户为无效用户,返回false
- 用户有效,类型为管理员,返回true
- 用户有效,类型为学生且为班长,返回true
- 用户有效,但为普通学生,返回false
- 用户有效,类型为教师且为组长,返回true
- 用户有效,类型为教师且为课程班带教老师,返回true
- 用户有效,但为普通教师,返回false
根据上面的测试用例,运行单元测试,NUnit运行如下图
测试用例全部通过,现在我修改一下生产代码,令运行失败,如下图
单元测试可以好清楚地显示哪个方法,哪个参数报错了,出错原因在右面。本人通过这两个例子为了显示一下单元测试在我们实际开发中的其中一个功能,快速定位bug。下面将开始讲解如何在实际项目中应用单元测试。
三、如何在实际项目中应用单元测试(.Net项目为例)
1、以NUnit为例,示范几个简单的例子
2、单元测试的核心技术
- 桩对象,是对系统中现有外部依赖的一个替代品,可人为控制
- 模拟对象,模仿外部依赖,属于一个伪对象,用于检验交互行为
- 隔离框架
NUnit标签介绍
- [TestFixture],用于标识一个包含NUnit自动化测试的类
- [TestCase],用于标识测试方法为一个参数化测试
- [ExpectedException],用于标识被测试方法应该抛出异常
- [Test],用于标识一个需要被调用的自动化测试
- [SetUp],会在测试类中的每个测试运行之前执行
- [TearDown],会在测试类中的每个测试运行之后执行
- [TestFixtureSetUp],会在测试类中的所有测试运行之前执行
- [TestFixtureTearDown],会在测试类中的所有测试运行之后执行
Assert类,用于证明某个假设是否成立
- Assert.IsTure(),用于验证结果是否为true
- Assert.IsFalse(),用于验证结果是否为false
- Assert.AreEqual(),用于验证期望的对象是否与实际一样
- Assert.AreSame (),用于验证两个参数引用是否为同一个对象
解除外部依赖的技巧
- 抽取接口,以允许替换底层实现
- 在被测类中注入中注入伪对象的实现
注入伪对象的几种方法
- 构造函数注入
- 属性注入
- 方法参数(参数注入)
- 工厂类注入
- 局部工厂方法注入(不讲)
- 抽取和重写注入
属性注入、参数注入和构造函数注入基本一致,只是注入的位置不一样。属性注入通过注入桩对象,参数注入通过方法的参数注入桩对象,下面看看工厂类注入
上面的例子,讲的都是桩对象的注入,其实模拟对象的注入跟桩对象是一样的,关键是要弄清楚桩对象和模拟对象的区别
桩对象的目的是解除外部依赖,为被测方法传入一个可控制的对象,让测试可以进行
模拟对象的目的是测试方法是否向外界发送了信息,检验交互行为,所以单元测试检验的对象不再是被测方法,而是模拟对象
隔离框架,用于快速生成桩对象和模拟对象,减少开发人员的负担,常用的隔离框架有Rhion Mocks、Moq等等