在单元测试中存根和模拟对象处于一个非常重要的地位以下我就来说说我对两者的理解。
工作单元最终的三种结果类型:
重点说一下交互测试:如果一个特定的工作单元最终的结果就是调用另一个对象那么就要进行交互测试。简单来说就是你无法判断你是否调用了这个方法,因为他的返回值是void,那么你只能通过其他方式来判断你确实调用了这个方法。这整个过程就就是交互测试。
什么是模拟对象:模拟对象也是伪对象,它可以验证被测试对象是否按照预期的方式调用了这个伪对象,因此来判断单元测试的成功或者失败。
举一个例子:小明由于作业没有做完,老师就让小明放学之后晚回家一个小时来写作业,那么今天老师有事就提前回家了,就让班长小亮来查看小明是否留下来一个小时在写作业。此时小亮就是我们说的伪对象,他就检测了小明是否晚回家一个小时。
1:存根上一篇已经说了现在我们看一下图
很明显我们断言的对象是被测试类下面是模拟对象
这个是对模拟对象的断言。
其实他们区别很小,他们的根本区别就是存根不会导致测试的失败而模拟对象却可以(存根由于断言是对被测试类所以不会导致测试失败,而模拟对象恰恰相反)
模拟对象就是来检测你的测试是否会失败。下面看例子
现在我引用一个外部的LogService专门记录错误日志,但是这个日志是void类型无法返回这个时候模拟对象就派上用场。我们先定义个一个日志服务接口
public interface ILogService { void ErrorLog(string message); }
被测试代码
PRivate readonly ILogService _logService; public UserManager(ILogService logService)//模拟对象注入 { this._logService = logService; } public void RecordLog(string userName) { if (userName.Length<=4)//名称长度小于4就记录日志异常 { _logService.ErrorLog(userName); } }
模拟对象
public class FakeLogService:ILogService { public string Message;//记录错误信息 public void ErrorLog(string message) { this.Message = message; } }
测试代码
[Category("模拟对象")] [Test] public void RecordLog_UserNameTooShort_CallLogService() { var mockService=new FakeLogService(); var userManager=new UserManager(mockService);//注入模拟对象 userManager.RecordLog("lp");//记录错误日志 Assert.AreEqual("lp", mockService.Message);//如果错误信息和模拟对象的相同说明我已经调用了这个方法并正确的传递了值 }
我们看一下测试效果:
我们发现测试过去了说明我们已经的方法正确的调用和传递值给日志服务。这个测试保证的是我们调用日志方法没有错误。
存根和模拟对象的同时使用
有时候一个方法体有2个未能返回值的方法,那么这个时候你可能就要确定一下哪个是存根哪个是模拟对象了。
比喻现在我们又加入一个需求,如果出现错误日志异常就要给系统管理员发一份邮件,这个时候我们就发现自己有2个没有返回值的函数,不建议写2个模拟对象,那样就会造成混乱你不知道到底是哪个方法出现错误(因为断言是针对于模拟对象的)
先定义一个邮件发送接口
public interface IEmailService { void SendEmail(string user, string subject, string content); }
模拟对象实现这个接口
public class FackEmailService : IEmailService { public string User { get; set; } public string Subject { get; set; } public string Content { get; set; } public void SendEmail(string user, string subject, string content) { this.User = user; this.Subject = subject; this.Content = content; } }
我们在看看被测试的代码
public class UserManager { public void RecordLog(string userName) { try { if (userName.Length <= 4) //名称长度小于4就记录日志异常 { LogManager().ErrorLog(userName); } } catch (Exception ex) { EmailService().SendEmail("lp", "subject", ex.Message); } } protected virtual ILogService LogManager()//底层可替换 { return new LogService(); }
protectedvirtual IEmailService EmailService()
{
returnnew EmailService();
} }
创建新类继承被测试类来完成底层替换
public class TestUserManager : UserManager { private readonly ILogService _logService; private readonly IEmailService _emailService; public TestUserManager(ILogService logService, IEmailService emailService) { _logService = logService; _emailService = emailService; } public override IEmailService EmailService() { return _emailService; } public override ILogService LogManager() { return _logService; } }
测试代码
[Test] public void RecordLog_EmailServiceThrows_SendsEmail() { var stubLogService = new FakeLogService() {Exception = new Exception("fack exception")};//日志的模拟对象抛出异常(这个是存根) var mokeEmailService = new FackEmailService(); var testUser = new TestUserManager(stubLogService, mokeEmailService);//注入存根和模拟对象 testUser.RecordLog("lp"); StringAssert.Contains("lp",mokeEmailService.User); StringAssert.Contains("subject",mokeEmailService.Subject); StringAssert.Contains("fack exception",mokeEmailService.Content); }
我们看看测试结果
你也可以把三个属性封装成一个实体对实体进行断言。
什么是对象链:就是一个对象的属性是另一个对象然后这个对象的属性又是一个对象。比喻我们经常看到的ConfigurationManager.ConnectionStrings[0].ConnectionString这个就是一个对象链。
如果我们在测试的时候就发现需要伪造2个对象如果很多的话就可能伪造的更多,所以我们在重构代码的时候就要考虑可测的代码如下这样就测试就可以直接替代
protected virtual string GetConnectionString() { return ConfigurationManager.ConnectionStrings[0].ConnectionString; }
新闻热点
疑难解答