在写单元测试的时候,如果被测试代码需要依赖外部环境(数据库、注册表、网络服务等)或者其它外部对象时,我们需要编写大量的代码来构建测试环境、构建被测试代码所依赖的外部对象,工作量大不说,而且编写的单元测试代码本身也可能非常脆弱,可能稍稍不小心,碰动了什么地方,就可能导致刚刚写的单元测试代码死活都绿不了(使用NUnit作为单元测试工具的时候,如果测试通过时,测试Case前面的图标会变成绿色的),更为离谱的是,如果希望单元测试代码能够在别的计算机上正常运行,还得为这段测试代码写一大段环境准备说明,天啦,这简直不是在写测试,而是在作一个目标系统的测试系统。如果再雪上加霜的遇到一个弱智设计,所有的代码都耦合到一起的话(比如所有的功能都写到了UI里面),那么恭喜你,中大奖了,如果这种情况下都能把单元测试做好,还有什么单元测试你做不好?
对于上面提到的这种情况下,在单元测试中已经有一个成熟的解决方案就是:写Mock对象。顾名思义,Mock对象就是假对象,就是替身。例如,如果我们要测试UserProfileCache类,这个UserProfileCache类的职责是维持UserProfile信息的缓存并且提供检索,代码如下:
我们都知道,对于面向对象软件设计而言,其中最核心的内容就是对象之间的交互,如果我们在写单元测试代码时,只测试了方法的输出而对方法内部引起的对象之间的交互不加以关注的话,那么就无法保证单元测试对代码的覆盖率,弱化了单元测试的作用,最终无法保证代码的质量。就以上面的单元测试代码为例,UserProfileCache类的Cache作用是否有效这个测试点没有被覆盖到,在手动写Mock对象的情况下,如果期望在单元测试中覆盖这个测试点,就需要在Mock中写大量的代码来实现这个逻辑,在这种情况下,就算不考虑工作量的问题,Mock对象中大量的测试代码又如何保证其正确性呢?难道在写测试代码的单元测试?那么测试代码的测试代码如何验证?似乎是一个鸡生蛋、蛋生鸡的问题。
为了避免这个问题,我们引入了DotNetMock框架,下面的示例代码演示了如何使用DotNetMock框架如何来对上面的示例代码做单元测试。
DotNetMock框架的核心就是IVerifiable接口,这个接口定义了一个可验证对象接口,在DotNetMock框架中所有的Mock类和Expection类都实现了这个接口,也就是说所有的Mock类和Expection类都是可以验证的。
DotNetMock框架中提供了两种类型的Mock类:静态Mock和动态Mock,其中MockObject类对应静态Mock,DynamicMock类对应动态Mock。对于静态Mock类,我个人认为用的地方不是太多,这个类实现了Expection的基本验证机制,当调用MockObject类的Verify方法时,MockObject类会调用类中所有的Expection的验证,在实际中MockObject类都是被当作基类来继承的,在大多数情况下直接使用DynamicMock类就可以满足要求,除非某些极其特殊的情况下才考虑手动写自定义Mock类(从MockObject类派生),下面的例子是使用MockObject类来实现Test_Lookup的例子,大家可以看一看两种实现方法的差别。
对于DynamicMock类的使用需要注意以下一点:
备注:DotNetMock框架需要与NUnit配合使用,比较可惜的是DotNetMock框架至今还没有推出.NET Framework2.0的版本。
对于上面提到的这种情况下,在单元测试中已经有一个成熟的解决方案就是:写Mock对象。顾名思义,Mock对象就是假对象,就是替身。例如,如果我们要测试UserProfileCache类,这个UserProfileCache类的职责是维持UserProfile信息的缓存并且提供检索,代码如下:
1 '用于提供用户信息的提供者
2 Public Interface IUserProfileProvider
3
4 '获得用户信息
5 Function GetProfile() As UserProfile()
6
7 End Interface
8
9 '用户信息的缓存对象
10 Public Class UserProfileCache
11
12 '默认的构造函数
13 Public Sub New(ByVal provider As IUserProfileProvider)
14 End Sub
15
16 '通过编号检索用户信息
17 Public Function Lookup(ByVal id As System.Guid) As UserProfile
18
19 '检索Profile
20 Dim profile As UserProfile = Me._profiles(id)
21
22 '判断是否有效
23 If profile Is Nothing Then
24
25 '获得列表
26 Dim tempItems As UserProfile() = Me._provider.GetProfile()
27
28 '判断是否为空
29 If Not tempItems Is Nothing Then
30
31 '更新缓存的内容
32 For Each profile In tempItems
33
34 '添加到Hash表中
35 Me._profiles.Remove(profile.Id)
36
37 '重新添加
38 Me._profiles.Add(profile.Id, profile)
39 Next
40 End If
41
42 Else
43
44 '返回数据
45 Return profile
46 End If
47
48 '返回数据
49 Return Me._profiles(id)
50
51 End Function
52
53 End Class
54
55 '用户信息
56 Public Class UserProfile
57
58 '编号
59 Public Id As System.Guid
60
61 '名称
62 Public Name As String
63
64 '用户数据
65 Public Data As String
66
67 End Class
在不使用Mock框架的情况下,我们需要编写如下的单元测试代码来测试CacheProfileCache类的Lookup方法:2 Public Interface IUserProfileProvider
3
4 '获得用户信息
5 Function GetProfile() As UserProfile()
6
7 End Interface
8
9 '用户信息的缓存对象
10 Public Class UserProfileCache
11
12 '默认的构造函数
13 Public Sub New(ByVal provider As IUserProfileProvider)
14 End Sub
15
16 '通过编号检索用户信息
17 Public Function Lookup(ByVal id As System.Guid) As UserProfile
18
19 '检索Profile
20 Dim profile As UserProfile = Me._profiles(id)
21
22 '判断是否有效
23 If profile Is Nothing Then
24
25 '获得列表
26 Dim tempItems As UserProfile() = Me._provider.GetProfile()
27
28 '判断是否为空
29 If Not tempItems Is Nothing Then
30
31 '更新缓存的内容
32 For Each profile In tempItems
33
34 '添加到Hash表中
35 Me._profiles.Remove(profile.Id)
36
37 '重新添加
38 Me._profiles.Add(profile.Id, profile)
39 Next
40 End If
41
42 Else
43
44 '返回数据
45 Return profile
46 End If
47
48 '返回数据
49 Return Me._profiles(id)
50
51 End Function
52
53 End Class
54
55 '用户信息
56 Public Class UserProfile
57
58 '编号
59 Public Id As System.Guid
60
61 '名称
62 Public Name As String
63
64 '用户数据
65 Public Data As String
66
67 End Class
1 <Test()> _
2 Public Sub Test_Lookup()
3
4 '创建Mock Provider
5 Dim provider As New MockProvider
6 provider.ExpectProfile = New UserProfile
7
8 '创建测试对象
9 Dim cache As New UserProfileCache(provider)
10
11 '断言
12 Assert.AreSame(cache.Lookup(provider.ExpectProfile.Id), provider.ExpectProfile)
13
14 End Sub
15
16 Public Class MockProvider
17 Implements IUserProfileProvider
18
19 Public ExpectProfile As UserProfile
20
21 Public Function GetProfile() As UserProfile() Implements IUserProfileProvider.GetProfile
22 Return New UserProfile() {Me.ExpectProfile}
23 End Function
24
25 End Class
从上面的实例代码可以看出在不使用Mock框架的情况下,我们需要手动的为引用类编写Mock类,在引用类数目比较少且引用类的接口比较简单的情况下这样做不失为一种好的处理方法,但是当引用类数目比较多且引用类的接口很复杂的情况下,再手动的写Mock类实在是太辛苦了。此外,我们写单元测试时,一般只关注被测试方法的输入输出或者状态的变化(例如,在上述的单元测试例子中关注了Lookup方法是否能够正确的检索到了UserProfile对象),但是对于被测试类与引用类之间的交互我们很少关注甚至不加以关注。2 Public Sub Test_Lookup()
3
4 '创建Mock Provider
5 Dim provider As New MockProvider
6 provider.ExpectProfile = New UserProfile
7
8 '创建测试对象
9 Dim cache As New UserProfileCache(provider)
10
11 '断言
12 Assert.AreSame(cache.Lookup(provider.ExpectProfile.Id), provider.ExpectProfile)
13
14 End Sub
15
16 Public Class MockProvider
17 Implements IUserProfileProvider
18
19 Public ExpectProfile As UserProfile
20
21 Public Function GetProfile() As UserProfile() Implements IUserProfileProvider.GetProfile
22 Return New UserProfile() {Me.ExpectProfile}
23 End Function
24
25 End Class
我们都知道,对于面向对象软件设计而言,其中最核心的内容就是对象之间的交互,如果我们在写单元测试代码时,只测试了方法的输出而对方法内部引起的对象之间的交互不加以关注的话,那么就无法保证单元测试对代码的覆盖率,弱化了单元测试的作用,最终无法保证代码的质量。就以上面的单元测试代码为例,UserProfileCache类的Cache作用是否有效这个测试点没有被覆盖到,在手动写Mock对象的情况下,如果期望在单元测试中覆盖这个测试点,就需要在Mock中写大量的代码来实现这个逻辑,在这种情况下,就算不考虑工作量的问题,Mock对象中大量的测试代码又如何保证其正确性呢?难道在写测试代码的单元测试?那么测试代码的测试代码如何验证?似乎是一个鸡生蛋、蛋生鸡的问题。
为了避免这个问题,我们引入了DotNetMock框架,下面的示例代码演示了如何使用DotNetMock框架如何来对上面的示例代码做单元测试。
1 <Test()> _
2 Public Sub Test_Lookup()
3
4 '创建Mock对象
5 Dim providerMock As New Dynamic.DynamicMock(GetType(IUserProfileProvider))
6
7 '创建临时对象
8 Dim tempUser As UserProfile = New UserProfile(System.Guid.NewGuid, "TempUser", "SomeDate")
9
10 '设置期望,期望第一次调用返回创建的临时对象
11 providerMock.ExpectAndReturn("GetProfile", New UserProfile() {tempUser})
12
13 '设置期望,期望不会有第二次调用
14 providerMock.ExpectNoCall("GetProfile")
15
16 '创建Cache
17 Dim cache As New UserProfileCache(providerMock.Object)
18
19 '检索用户
20 Dim actual As UserProfile = cache.Lookup(tempUser.Id)
21
22 '断言检索的结果正确
23 Assert.IsNotNull(actual, "没有检索到数据")
24
25 '断言检索的对象相同
26 Assert.AreSame(tempUser, actual, "检索的对象错误")
27
28 '再调用一次
29 actual = cache.Lookup(tempUser.Id)
30
31 '断言检索的结果正确
32 Assert.IsNotNull(actual, "没有检索到数据")
33
34 '断言检索的对象相同
35 Assert.AreSame(tempUser, actual, "检索的对象错误")
36
37 '断言Mock对象所有的期望都满足
38 providerMock.Verify()
39
40 End Sub
对于上面的代码,没有使用DotNetMock框架经验的兄弟可能看不太明白,不过不要紧,后面我会详细的介绍一下DotNetMock框架的一些常用用法。在上面的代码中,有几行代码特别的用红色标注出来,其中“ Dim providerMock As New Dynamic.DynamicMock(GetType(IUserProfileProvider))”这个语句的含义是创建一个动态Mock对象,并且要求这个对象实现接口“IUserProfileProvider”(DotNetMock框架提供的很酷的功能,避免了手动写一个Mock类来实现IUserProfileProvider接口),后面的两个语句“providerMock.ExpectAndReturn("GetProfile", New UserProfile() {tempUser})”和“providerMock.ExpectNoCall("GetProfile")”的含义是添加两个期望(Expectation),前一个语句添加的期望是:如果调用IUserProfileProvider接口的GetProfile方法,则返回创建的对象数组(New UserProfile() {tempUser}),后一个语句添加的期望是:不希望调用IUserProfileProvider接口的GetProfile方法,这个期望断言了再次检索同一个ID的用户信息时,缓存发生了作用,不会再次调用GetProfile方法。从上面的代码可以看出,使用DotNetMock框架可以大幅度的降低需要使用Mock对象的单元测试的代码量,并且代码优雅可靠的多。是不是很酷,好,接下来,我们一起来探讨以下DotNetMock框架的常见用法。2 Public Sub Test_Lookup()
3
4 '创建Mock对象
5 Dim providerMock As New Dynamic.DynamicMock(GetType(IUserProfileProvider))
6
7 '创建临时对象
8 Dim tempUser As UserProfile = New UserProfile(System.Guid.NewGuid, "TempUser", "SomeDate")
9
10 '设置期望,期望第一次调用返回创建的临时对象
11 providerMock.ExpectAndReturn("GetProfile", New UserProfile() {tempUser})
12
13 '设置期望,期望不会有第二次调用
14 providerMock.ExpectNoCall("GetProfile")
15
16 '创建Cache
17 Dim cache As New UserProfileCache(providerMock.Object)
18
19 '检索用户
20 Dim actual As UserProfile = cache.Lookup(tempUser.Id)
21
22 '断言检索的结果正确
23 Assert.IsNotNull(actual, "没有检索到数据")
24
25 '断言检索的对象相同
26 Assert.AreSame(tempUser, actual, "检索的对象错误")
27
28 '再调用一次
29 actual = cache.Lookup(tempUser.Id)
30
31 '断言检索的结果正确
32 Assert.IsNotNull(actual, "没有检索到数据")
33
34 '断言检索的对象相同
35 Assert.AreSame(tempUser, actual, "检索的对象错误")
36
37 '断言Mock对象所有的期望都满足
38 providerMock.Verify()
39
40 End Sub
DotNetMock框架的核心就是IVerifiable接口,这个接口定义了一个可验证对象接口,在DotNetMock框架中所有的Mock类和Expection类都实现了这个接口,也就是说所有的Mock类和Expection类都是可以验证的。
DotNetMock框架中提供了两种类型的Mock类:静态Mock和动态Mock,其中MockObject类对应静态Mock,DynamicMock类对应动态Mock。对于静态Mock类,我个人认为用的地方不是太多,这个类实现了Expection的基本验证机制,当调用MockObject类的Verify方法时,MockObject类会调用类中所有的Expection的验证,在实际中MockObject类都是被当作基类来继承的,在大多数情况下直接使用DynamicMock类就可以满足要求,除非某些极其特殊的情况下才考虑手动写自定义Mock类(从MockObject类派生),下面的例子是使用MockObject类来实现Test_Lookup的例子,大家可以看一看两种实现方法的差别。
1 <Test()> _
2 Public Sub Test_Lookup2()
3
4 '创建临时对象
5 Dim tempUser As UserProfile = New UserProfile(System.Guid.NewGuid, "TempUser", "SomeDate")
6
7 '创建Mock对象
8 Dim providerMock As New MockProvider(New UserProfile() {tempUser})
9
10 '创建Cache
11 Dim cache As New UserProfileCache(providerMock)
12
13 '检索用户
14 Dim actual As UserProfile = cache.Lookup(tempUser.Id)
15
16 '断言检索的结果正确
17 Assert.IsNotNull(actual, "没有检索到数据")
18
19 '断言检索的对象相同
20 Assert.AreSame(tempUser, actual, "检索的对象错误")
21
22 '再调用一次
23 actual = cache.Lookup(tempUser.Id)
24
25 '断言检索的结果正确
26 Assert.IsNotNull(actual, "没有检索到数据")
27
28 '断言检索的对象相同
29 Assert.AreSame(tempUser, actual, "检索的对象错误")
30
31 '验证Mock对象
32 providerMock.Verify()
33
34 End Sub
35
36 Public Class MockProvider
37 Inherits MockObject
38 Implements IUserProfileProvider
39
40 Public Sub New(ByVal values As UserProfile())
41
42 '设置返回值
43 Me._values = values
44
45 '设置期望的调用次数,只期望调用一次
46 Me._expectation.Expected = 1
47 End Sub
48
49 '预先设定的数值
50 Private _values As UserProfile()
51
52 '调用次数计数
53 Private _expectation As New ExpectationCounter("计数")
54
55 Public Function GetProfile() As MockUnitTestDemo.UserProfile() Implements MockUnitTestDemo.IUserProfileProvider.GetProfile
56
57 '增加调用计数
58 Me._expectation.Inc()
59
60 '返回结果
61 Return Me._values
62 End Function
63
64 End Class
是不是看起来比使用DynamicMock类要复杂而且代码量也大一些。不过使用静态Mock的话,可以使用很多DotNetMock框架提供的Expectation类,具体的可以参考DotNetMock框架文档。另外,还有一种比较好的用法是直接从DynamicMock类派生,同时拥有动态Mock提供的便利性,又可以拥有静态Mock提供的灵活行。2 Public Sub Test_Lookup2()
3
4 '创建临时对象
5 Dim tempUser As UserProfile = New UserProfile(System.Guid.NewGuid, "TempUser", "SomeDate")
6
7 '创建Mock对象
8 Dim providerMock As New MockProvider(New UserProfile() {tempUser})
9
10 '创建Cache
11 Dim cache As New UserProfileCache(providerMock)
12
13 '检索用户
14 Dim actual As UserProfile = cache.Lookup(tempUser.Id)
15
16 '断言检索的结果正确
17 Assert.IsNotNull(actual, "没有检索到数据")
18
19 '断言检索的对象相同
20 Assert.AreSame(tempUser, actual, "检索的对象错误")
21
22 '再调用一次
23 actual = cache.Lookup(tempUser.Id)
24
25 '断言检索的结果正确
26 Assert.IsNotNull(actual, "没有检索到数据")
27
28 '断言检索的对象相同
29 Assert.AreSame(tempUser, actual, "检索的对象错误")
30
31 '验证Mock对象
32 providerMock.Verify()
33
34 End Sub
35
36 Public Class MockProvider
37 Inherits MockObject
38 Implements IUserProfileProvider
39
40 Public Sub New(ByVal values As UserProfile())
41
42 '设置返回值
43 Me._values = values
44
45 '设置期望的调用次数,只期望调用一次
46 Me._expectation.Expected = 1
47 End Sub
48
49 '预先设定的数值
50 Private _values As UserProfile()
51
52 '调用次数计数
53 Private _expectation As New ExpectationCounter("计数")
54
55 Public Function GetProfile() As MockUnitTestDemo.UserProfile() Implements MockUnitTestDemo.IUserProfileProvider.GetProfile
56
57 '增加调用计数
58 Me._expectation.Inc()
59
60 '返回结果
61 Return Me._values
62 End Function
63
64 End Class
对于DynamicMock类的使用需要注意以下一点:
- DynamicMock可以模拟接口和类,需要注意的是如果模拟的是类的话,需要注意是需要添加Expetation的方法是否为虚方法(虚方法指的是可以重载的方法,DynamicMock的实现机制就是运行时动态的产生一个被模拟类的派生类并且重载指定方法、安插Expectation,所以如果指定的方法不能被重载,那么安插的Expectation就不会起效)
- 在DynamicMock类上面添加的Expectation,每一次调用都会使用掉一个Expectation,例如上面的例子中的“ providerMock.ExpectAndReturn("GetProfile", New UserProfile() {tempUser})”,在调用一次GetProfile方法之后就被从DynamicMock类的实例中删除掉了(满足期望的前提下),如果有多个Expectation,就需要调用对应的方法多次(ExpectNoCall除外),其中DynamicMock的Verify方法检查了所有的Expectation是否都被执行(从实例中删除了,ExpectNoCall除外)
- 另外需要注意的是DynamicMock类的SetValue方法,如果在指定方法上调用了SetValue方法,那么在这个方法上就不能挂接Expectation了。SetValue方法的含义是指定了方法的返回值,以后每次调用指定方法的时候都会返回指定的返回值,主要用作模拟接口实现。
- 对于属性、事件等成员的测试,需要使用静态Mock或者自行从DynamicMock类派生实现。
- SetValue,设定指定方法的实现,例如 Mock.SetValue("GetName", "张山"),多次调用SetValue,仅仅离被指定方法调用最近的调用起效,例如下面的代码,GetName的结果为“李四”。
1 Mock.SetValue("GetName", "张三")
2 Mock.SetValue("GetName", "李四")
3 Mock.Object.GetName()
4 Mock.SetValue("GetName", "王五") - Expect,在指定方法上添加Expetation,期望以特定的参数调用特定的方法
- ExpectAndReturn,在指定方法上添加Expetation,期望以特定的参数调用特定的方法,并且返回特定的数值
- ExpectNoCall,在指定方法上添加Expetation,期望特定的方法不被调用
- ExpectAndThrow,在指定方法上添加Expetation,期望在特定的参数调用特定的方法的情况下,抛出特定的异常
- NotImplemented,在指定方法上添加Expetation,指定特定的方法没有被实现,调用时抛出NotImplement异常
- Verify,判断Mock对象的所有Expetation是否都被验证
- Call,调用模拟对象的特定方法,单元测试代码中调用方法都是通过Call方法简介实现的
备注:DotNetMock框架需要与NUnit配合使用,比较可惜的是DotNetMock框架至今还没有推出.NET Framework2.0的版本。