打破依赖,使用模拟对象,桩对象,隔离框架
在上节中,完成了第一个单元测试,研究了各种特性,在本节,将介绍一些更实际的例子。SUT依赖于一个不可操控的对象,最常见的例子是文件系统,线程,内存和时间等。
本系列将分成3节:
- 单元测试基础知识
- 打破依赖,使用模拟对象,桩对象,隔离框架
- 创建优秀的单元测试
本节索引:
伪对象(fake) 桩对象(stub) 模拟对象(mock)
伪对象是一个通用术语,它即可指桩对象,也可指模拟对象。
桩对象是指对系统中现有依赖项的一个替代品,可人为控制。
模拟对象是用来决定一个单元测试是通过还是失败的伪对象。
说明:fake是stub和mock的统称,因为看起来都像是真的对象。如果是用来检查交互的就是模拟对象,否则就是桩对象
桩对象:
模拟对象:
- 外部依赖(系统中代码与其交互的对象,而且无法对其做人为控制)
- 反测试(而一旦测试中存在外部依赖,那么这个测试就是一个集成测试。运行慢,需要配置,依赖异常)
如何处理?
本质上都是外部依赖导致的,所以要做的是消除依赖。
- 分析接口
- 实现可人为控制的接口
注入桩对象
- 在构造函数上接受一个接口,并保存在一个字段里,以备后用。
- 保存在属性上
- 在调用方法前,使用方法参数,工厂类,依赖注入等
隐藏桩对象(由于生产环境等其他原因,我们不希望暴露桩对象)
- 使用条件编译
- 使用条件特性
- 使用internal和[InternalVisibleTo]
使用桩对象(适用于模拟返回值,不适用于检查对象间的交互情况。)
这是非常常见的方式,但是这种方式受限制很多,如文件需要配置,运行慢。
1
2
3
4
5
6
7
8
|
public class Config { public bool IsCheck( string name) { var str = File.ReadAllText( "1.txt" ); return str == name; //此处可能是大量的逻辑处理 } } |
改写注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public class Config { private IManager manager; //提供注入接口 public Config(IManager manager) { this .manager = manager; } public bool IsCheck( string name) { var str = manager.GetConfig(); return str == name; } } //真实的实现 public class FileManager : IManager { public string GetConfig() { return File.ReadAllText( "1.txt" ); } } //测试使用的实现 public class StubManager : IManager { public string GetConfig() { return "str" ; } } //抽象出的接口 public interface IManager { string GetConfig(); } |
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
[TestClass] public class ConfigTests { private Config config; [TestInitialize] public void Init() { config = new Config( new StubManager()); } [TestMethod] public void IsCheckTest() { Assert.IsTrue(config.IsCheck( "str" )); } [TestCleanup] public void Clean() { config = null ; } } |
使用模拟对象(适用于对象之间的交互)
当上面的方法返回false的时候,需要调用别的web服务记录下。而web服务还未开发好,即使开发好了,测试的时间也会变长很多。
这里其实也体现了,stub的优点,可以任意的控制返回结果。
新建一个mock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class Config { private IManager manager; public IWeb Web { get ; set ; } public Config(IManager manager) { this .manager = manager; } public bool IsCheck( string name) { var str = manager.GetConfig(); var rst = str == name; if (!rst) Web.Log( "错误" ); return rst; } } /// <summary> /// 模拟对象 /// </summary> public class MockWeb : IWeb { public string Erro { get ; set ; } public void Log( string erro) { Erro = erro; } } public interface IWeb { void Log( string erro); } |
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[TestClass] public class WebTests { [TestMethod] public void LogTest() { var web = new MockWeb(); //注入的方式非常多 var config = new Config( new StubManager()) { Web = web }; config.IsCheck( "s" ); //最终断言的是模拟对象。 Assert.AreEqual( "错误" , web.Erro); } } |
注意:一个测试只有一个mock,其他伪对象都是stub,如果存在多个mock,说明这个单元测试是在测多个事情,这样会让测试变得复杂和脆弱。
隔离框架简介
手写stub和mock非常麻烦耗时,而且不易看懂等缺点。
隔离框架是可以方便的新建stub和mock的一组可编程API。
.net下常见的有Rhino Mocks,Moq
这里使用RhinoMocks做示例(将使用录制回放模式和操作断言2种)
录制回放
新建mock对象
来实现一个和上面mock的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[TestMethod] public void LogMockTest() { var mocks = new MockRepository();<br> //严格模拟对象 var mockWeb = mocks.StrictMock<IWeb>(); using (mocks.Record()) //录制预期行为 { mockWeb.Log( "错误" ); } var config = new Config( new StubManager()) { Web = mockWeb }; config.IsCheck( "s" ); mocks.Verify(mockWeb); } |
严格模拟对象:是指只要出现预期行为以外的情况,就报错。
非严格模拟对象:是指执行到最后一行,才会报错。
新建stub对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
[TestMethod] public void LogStubTest() { var mocks = new MockRepository(); //非严格对象 var mockWeb = mocks.DynamicMock<IWeb>(); //桩对象 var stubManager = mocks.Stub<IManager>(); using (mocks.Record()) { mockWeb.Log( "错误1" ); stubManager.GetConfig(); LastCall.Return( "str1" ); //录制桩对象返回值 } var config = new Config(stubManager) { Web = mockWeb }; config.IsCheck( "str" ); mocks.Verify(stubManager); //桩对象不会导致测试失败 mocks.VerifyAll(); //启用非严格对象,测试直到这里才会确认是否报错 } |
操作断言
1
2
3
4
5
6
7
8
9
10
11
12
|
[TestMethod] public void LogReplayTest() { var mocks = new MockRepository(); var mockWeb = mocks.DynamicMock<IWeb>(); var config = new Config( new StubManager()) { Web = mockWeb }; //开始操作模式 mocks.ReplayAll(); config.IsCheck( "str1" ); //使用Rhino Mocks断言 mockWeb.AssertWasCalled(o => o.Log( "错误" )); } |
注意:使用框架创建的动态伪对象,肯定没手工编写的伪对象执行效率高。