基于值、状态和交互的测试
- 基于值的测试:验证一个函数的返回值
- 基于状态的测试:改变被测系统的状态,然后验证其可见的行为变化。
- 交互测试:是对一个对象如何向其他对象发送消息(调用方法)的测试。
交互测试
对一个灌溉系统测试来比较基于状态的测试和交互测试的区别
- 基于状态的集成测试:让灌溉系统运行12小时,这期间它应该给树浇很多次水。这个时间结束后,检查灌溉的树的状态,土壤是否足够潮湿,树是否健康,树叶是否青翠,等等。
- 交互测试:在每个水管末端,安装一个设备,记录什么时间多少水流过。每天结束时,检查这个设备记录的次数是否正确,每次记录的水量是否正确,不用费心检查树的情况。实际上,不需要树也可以检查系统是否工作。进一步修改灌溉单元上的系统时钟(使其成为一个桩),让系统以为灌溉时间已到,在选择的任意时间进行灌溉。这样无需等待(等12小时)即可知道系统是否工作。
模拟对象定义
在上面的记录灌溉信息的设备就是模拟对象,模拟对象是系统中的伪对象,它可以验证被测试对象是否按照预期的方式调用了这个伪对象,因此导致单元测试通过或失败。(通常每个测试最多有一个模拟对象)
伪对象(Fake)定义
伪对象是桩和模拟对象的统称,因为桩和模拟对象都看上去很像真是的对象。如果这个伪对象用来检验一个交互(对其断言),它就是模拟对象(Mock),否则就是桩(Stub)。
模拟对象和桩的区别
- 桩:测试是针对被测试类进行断言的。因此桩永远不会导致测试失败。
- 模拟对象:测试是针对模拟对象进行断言的。模拟对象只比桩多做一件事:它保存通讯的历史记录,这些记录之后用于验证。
实例
新需求
LogAnalyzer在每次遇到过短文件名时,向一个Web服务发送一个错误消息。
首先抽取一个简单接口
public interface IWebService
{
void LogError(string message);
}
然后定义模拟对象
public class FakeWebService:IWebService
{
public string LastError;
public void LogError(string message)
{
LastError = message;
}
}
编制被测试类
public class LogAnalyzerMockOnly
{
private IWebService service;
public LogAnalyzerMockOnly(IWebService service)
{
this.service = service;
}
public void Analyze(string fileName)
{
if (fileName.Length < 8)
{
service.LogError("文件名太短:" + fileName);
}
}
}
测试代码
public class LogAnalyzerMockOnlyTest
{
public void Analyze_TooShortFileName_CallsWebService()
{
//先实例化伪对象
FakeWebService mockService = new FakeWebService();
//然后使用实例化LogAnalyzer并用伪对象构造
LogAnalyzerMockOnly log = new LogAnalyzerMockOnly(mockService);
string tooShortFileName = "abc.ext";
log.Analyze(tooShortFileName);
//对模拟对象断言,模拟对象中的LastError记录了调用log.Analyze是产生的错误值
StringAssert.Contains("文件名太短:abc.ext", mockService.LastError);
//断言没有写在模拟对象内部的原因是:
//* 其他测试用例能够使用别的断言,重用这个模拟对象
//* 断言写在伪造对象内部的话,测试代码的可读性和可维护性降低了。
}
}