C# 单元测试 交互测试-模拟对象
二、交互测试-模拟对象
工作单元可能有三种最终结果,目前为止,我们编写过的测试只针对前两种:返回值和改变系统状态。现在,我们来了解如何测试第三种最终结果-调用第三方对象。
2.1 模拟对象与存根的区别
模拟对象和存根之间的区别很小,但二者之间的区别非常微妙,但又很重要。二者最根本的区别在于:
存根不会导致测试失败,而模拟对象可以。
下图展示了存根和模拟对象之间的区别,可以看到测试会使用模拟对象验证测试是否失败。
2.2 第一个手工模拟对象
创建和使用模拟对象的方法与使用存根类似,只是模拟对象比存根多做一件事:它保存通讯的历史记录,这些记录之后用于预期(Expection)验证。
假设我们的被测试项目LogAnalyzer需要和一个外部的Web Service交互,每次LogAnalyzer遇到一个过短的文件名,这个Web Service就会收到一个错误消息。遗憾的是,要测试的这个Web Service还没有完全实现。就算实现了,使用这个Web Service也会导致测试时间过长。
因此,我们需要重构设计,创建一个新的接口,之后用于这个接口创建模拟对象。这个接口只包括我们需要调用的Web Service方法。
Step1.抽取接口,被测试代码可以使用这个接口而不是直接调用Web Service。然后创建实现接口的模拟对象,它看起来十分像存根,但是它还存储了一些状态信息,然后测试可以对这些信息进行断言,验证模拟对象是否正确调用。
public interface IWebService
{
void LogError(string message);
}
public class FakeWebService : IWebService
{
public string LastError;
public void LogError(string message)
{
this.LastError = message;
}
}
Step2.在被测试类中使用依赖注入(这里是构造函数注入)消费Web Service:
public class LogAnalyzer
{
private IWebService service;
public LogAnalyzer(IWebService service)
{
this.service = service;
}
public void Analyze(string fileName)
{
if (fileName.Length < 8)
{
// 在产品代码中写错误日志
service.LogError(string.Format("Filename too short : {0}",fileName));
}
}
}
Step3.使用模拟对象测试LogAnalyzer:
[Test]
public void Analyze_TooShortFileName_CallsWebService()
{
FakeWebService mockService = new FakeWebService();
LogAnalyzer log = new LogAnalyzer(mockService);
string tooShortFileName = "abc.ext";
log.Analyze(tooShortFileName);
// 使用模拟对象进行断言
StringAssert.Contains("Filename too short : abc.ext", mockService.LastError);
}
可以看出,这里的测试代码中我们是对模拟对象进行断言,而非LogAnalyzer类,因为我们测试的是LogAnalyzer和Web Service之间的交互。
2.3 同时使用模拟对象和存根
假设我们得LogAnalyzer不仅需要调用Web Service,而且如果Web Service抛出一个错误,LogAnalyzer还需要把这个错误记录在另一个外部依赖项里,即把错误用电子邮件发送给Web Service管理员,如下代码所示:
if (fileName.Length < 8)
{
try
{
// 在产品代码中写错误日志
service.LogError(string.Format("Filename too short : {0}", fileName));
}
catch (Exception ex)
{
email.SendEmail("a", "subject", ex.Message);
}
}
可以看出,这里LogAnalyzer有两个外部依赖项:Web Service和电子邮件服务。我们看到这段代码只包含调用外部对象的逻辑,没有返回值,也没有系统状态的改变,那么我们如何测试当Web Service抛出异常时LogAnalyzer正确地调用了电子邮件服务呢?
我们可以在测试代码中使用存根替换Web Service来模拟异常,然后模拟邮件服务来检查调用。测试的内容是LogAnalyzer与其他对象的交互。
Step1.抽取Email接口,封装Email类
public interface IEmailService
{
void SendEmail(EmailInfo emailInfo);
}
public class EmailInfo
{
public string Body;
public string To;
public string Subject;
public EmailInfo(string to, string subject, string body)
{
this.To = to;
this.Subject = subject;
this.Body = body;
}
public override bool Equals(object obj)
{
EmailInfo compared = obj as EmailInfo;
return To == compared.To && Subject == compared.Subject
&& Body == compared.Body;
}
}
Step2.封装EmailInfo类,重写Equals方法
public class EmailInfo
{
public string Body;
public string To;
public string Subject;
public EmailInfo(string to, string subject, string body)
{
this.To = to;
this.Subject = subject;
this.Body = body;
}
public override bool Equals(object obj)
{
EmailInfo compared = obj as EmailInfo;
return To == compared.To && Subject == compared.Subject
&& Body == compared.Body;
}
}
Step3.创建FakeEmailService模拟对象,改造FakeWebService为存根
public class FakeEmailService : IEmailService
{
public EmailInfo email = null;
public void SendEmail(EmailInfo emailInfo)
{
this.email = emailInfo;
}
}
public class FakeWebService : IWebService
{
public Exception ToThrow;
public void LogError(string message)
{
if (ToThrow != null)
{
throw ToThrow;
}
}
}
Step4.改造LogAnalyzer类适配两个Service
public class LogAnalyzer
{
private IWebService webService;
private IEmailService emailService;
public LogAnalyzer(IWebService webService, IEmailService emailService)
{
this.webService = webService;
this.emailService = emailService;
}
public void Analyze(string fileName)
{
if (fileName.Length < 8)
{
try
{
webService.LogError(string.Format("Filename too short : {0}", fileName));
}
catch (Exception ex)
{
emailService.SendEmail(new EmailInfo("someone@qq.com", "can't log", ex.Message));
}
}
}
}
Step5.编写测试代码,创建预期对象,并使用预期对象断言所有的属性
[Test]
public void Analyze_WebServiceThrows_SendsEmail()
{
FakeWebService stubService = new FakeWebService();
stubService.ToThrow = new Exception("fake exception");
FakeEmailService mockEmail = new FakeEmailService();
LogAnalyzer log = new LogAnalyzer(stubService, mockEmail);
string tooShortFileName = "abc.ext";
log.Analyze(tooShortFileName);
// 创建预期对象
EmailInfo expectedEmail = new EmailInfo("someone@qq.com", "can't log", "fake exception");
// 用预期对象同时断言所有属性
Assert.AreEqual(expectedEmail, mockEmail.email);
}
总结:每个测试应该只测试一件事情,测试中应该也最多只有一个模拟对象。一个测试只能指定工作单元三种最终结果中的一个,不然的话天下大乱。
出处:http://edisonchou.cnblogs.com