在单元测试中存根和模拟对象处于一个非常重要的地位以下我就来说说我对两者的理解。
1:什么是交互测试工作单元最终的三种结果类型:
基于值的测试:验证函数返回值基于状态的测试:验证通过被测试函数之后状态的变化交互测试:验证一个对象如何向另一个对象(一般是第三方服务)发送消息(调用方法)重点说一下交互测试:如果一个特定的工作单元最终的结果就是调用另一个对象那么就要进行交互测试。简单来说就是你无法判断你是否调用了这个方法,因为他的返回值是void,那么你只能通过其他方式来判断你确实调用了这个方法。这整个过程就就是交互测试。
2:模拟对象什么是模拟对象:模拟对象也是伪对象,它可以验证被测试对象是否按照预期的方式调用了这个伪对象,因此来判断单元测试的成功或者失败。
举一个例子:小明由于作业没有做完,老师就让小明放学之后晚回家一个小时来写作业,那么今天老师有事就提前回家了,就让班长小亮来查看小明是否留下来一个小时在写作业。此时小亮就是我们说的伪对象,他就检测了小明是否晚回家一个小时。
3:模拟对象和存根的区别1:存根上一篇已经说了现在我们看一下图
[flash=500,400]http://s1.knowsky.com/20160211/p14pc5wdpay06.png[/flash]
很明显我们断言的对象是被测试类下面是模拟对象
[flash=500,400]http://s1.knowsky.com/20160211/u2ttlomv0vn06.png[/flash]
这个是对模拟对象的断言。
其实他们区别很小,他们的根本区别就是存根不会导致测试的失败而模拟对象却可以(存根由于断言是对被测试类所以不会导致测试失败,而模拟对象恰恰相反)
模拟对象就是来检测你的测试是否会失败。下面看例子
4:模拟对象和存根的使用现在我引用一个外部的LogService专门记录错误日志,但是这个日志是void类型无法返回这个时候模拟对象就派上用场。我们先定义个一个日志服务接口
publicinterfaceILogService
{voidErrorLog(stringmessage);
}
被测试代码
PRivatereadonlyILogService _logService;publicUserManager(ILogService logService)//模拟对象注入{this._logService =logService;
}publicvoidRecordLog(stringuserName)
{if(userName.Length<=4)//名称长度小于4就记录日志异常{
_logService.ErrorLog(userName);
}
}
模拟对象
publicclassFakeLogService:ILogService
{publicstringMessage;//记录错误信息publicvoidErrorLog(stringmessage)
{this.Message =message;
}
}
测试代码
[Category("模拟对象")]
[Test]publicvoidRecordLog_UserNameTooShort_CallLogService()
{varmockService=newFakeLogService();varuserManager=newUserManager(mockService);//注入模拟对象userManager.RecordLog("lp");//记录错误日志Assert.AreEqual("lp", mockService.Message);//如果错误信息和模拟对象的相同说明我已经调用了这个方法并正确的传递了值}
我们看一下测试效果:
[flash=500,400]http://s1.knowsky.com/20160211/uyveirnonr206.jpg[/flash]
我们发现测试过去了说明我们已经的方法正确的调用和传递值给日志服务。这个测试保证的是我们调用日志方法没有错误。
存根和模拟对象的同时使用
有时候一个方法体有2个未能返回值的方法,那么这个时候你可能就要确定一下哪个是存根哪个是模拟对象了。
比喻现在我们又加入一个需求,如果出现错误日志异常就要给系统管理员发一份邮件,这个时候我们就发现自己有2个没有返回值的函数,不建议写2个模拟对象,那样就会造成混乱你不知道到底是哪个方法出现错误(因为断言是针对于模拟对象的)
先定义一个邮件发送接口
publicinterfaceIEmailService
{voidSendEmail(stringuser,stringsubject,stringcontent);
}
模拟对象实现这个接口
publicclassFackEmailService : IEmailService
{publicstringUser {get;set; }publicstringSubject {get;set; }publicstringContent {get;set; }publicvoidSendEmail(stringuser,stringsubject,stringcontent)
{this.User =user;this.Subject =subject;this.Content =content;
}
}
我们在看看被测试的代码
publicclassUserManager
{publicvoidRecordLog(stringuserName)
{try{if(userName.Length <=4)//名称长度小于4就记录日志异常{
LogManager().ErrorLog(userName);
}
}catch(Exception ex)
{
EmailService().SendEmail("lp","subject", ex.Message);
}
}
protectedvirtualILogService LogManager()//底层可替换{returnnewLogService();
}
protectedvirtualIEmailService EmailService()
{
returnnewEmailService();
} }
创建新类继承被测试类来完成底层替换
publicclassTestUserManager : UserManager
{privatereadonlyILogService _logService;privatereadonlyIEmailService _emailService;publicTestUserManager(ILogService logService, IEmailService emailService)
{
_logService=logService;
_emailService=emailService;
}publicoverrideIEmailService EmailService()
{return_emailService;
}publicoverrideILogService LogManager()
{return_logService;
}
}
测试代码
[Test]publicvoidRecordLog_EmailServiceThrows_SendsEmail()
{varstubLogService =newFakeLogService() {Exception =newException("fack exception")};//日志的模拟对象抛出异常(这个是存根)varmokeEmailService =newFackEmailService();vartestUser =newTestUserManager(stubLogService, mokeEmailService);//注入存根和模拟对象testUser.RecordLog("lp");
StringAssert.Contains("lp",mokeEmailService.User);
StringAssert.Contains("subject",mokeEmailService.Subject);
StringAssert.Contains("fack exception",mokeEmailService.Content);
}
我们看看测试结果
[flash=500,400]http://s1.knowsky.com/20160211/xspzd5s4hmo07.jpg[/flash]
你也可以把三个属性封装成一个实体对实体进行断言。
5:伪对象链什么是对象链:就是一个对象的属性是另一个对象然后这个对象的属性又是一个对象。比喻我们经常看到的ConfigurationManager.ConnectionStrings[0].ConnectionString这个就是一个对象链。
如果我们在测试的时候就发现需要伪造2个对象如果很多的话就可能伪造的更多,所以我们在重构代码的时候就要考虑可测的代码如下这样就测试就可以直接替代
protectedvirtualstringGetConnectionString()
{returnConfigurationManager.ConnectionStrings[0].ConnectionString;
}
6:手工模拟对象和存根的存在的问题编写模拟对象和存根耗时间如果接口有很多方法、属性、事件编写的时候会特别困难如果验证调用者向另一个方法调用传递的所有参数都是正确的时候就需要多次进行断言。有些模拟对象就是为特定的方法编写复用性比较差