使用Nmock单元测试 .NET 业务对象
Nmock是在开发环境下测试复杂业务对象的唯一方式
理解单元测试问题
在一个测试驱动的开发环境(test-driven developmentenvironment)下,为复杂的业务对象编写单元测试脚本很困难,因为,业务对象可能外部依赖(高耦合,high coupling)很多其他对象。
有时,对于有限的项目预算来说,建立开发环境,并配置它们的单元测试,不大可能。但是开发人员可以通过 mock 测试对象解决这个问题。
通过创建 mock 业务对象,而不是你复杂业务对象的真正实现,你可以一次测试一个类。Mock 对象是模拟对象,复制你的业务对象的行为,你可以在你真正的单元测试脚本中使用 这些mock 对象。
使用 Nmock 库
很少有微软 .NET 支持的模拟(mock)测试框架——Nmock 就是其中之一。Nmock 也可以很容易与 Nunit 测试框架集成。Nmock 是免费的。
Nmock是一个独特的具有以下特征的模拟测试库:
- 同其他单元测试框架一样,开发人员需要定义期望。你期待什么作为输出?
- 模拟对象基于实现的接口,本质上是动态的。
- 如果实际的输出与预期不符合,那么你的测试用例将失败。
- 错误信息显示错误的详细描述。
安装 Nmock 测试环境
为了开发示例应用程序,使用 VisualStudio 2008 .NET 框架3.5。单元测试使用 NUnit 2.5.10。在安装NUnit msi包之后,除了能看到 nunit.framework.dll的引用,还能看到nunit.mocks.dll,但本示例项目中只引用 nunit.framework.dll。
下载 Nmock 库,解压后,会得到一些 DLL 文件。在本示例项目中,只增加 NMock2.dll 的引用。本例演示常见的长度和距离的转换系数,将一个长度计量单位转换到另一个的转换系数。例如,米到英尺的转换系数是3.28。
首先创建一个C#控制台程序。新建接口 IconversionService,代码如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4:
5: namespace MockTesting.Sample
6: {
7: public interface IconversionService
8: {
9: double GetConversionRate ( string fromlength, string tolength );
10: }
11:
12: }
说明:GetConversionRate 方法返回转换因子。
新建类 ConversionServices,代码如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4:
5: namespace MockTesting.Sample
6: {
7: public class ConversionServices : IconversionService
8: {
9:
10: public double GetConversionRate ( string fromlength, string tolength )
11: {
12: if ( fromlength.Length <=0 || tolength.Length <= 0 )
13: return 0;
14:
15: switch(fromlength)
16: {
17: case "MET":
18: if (tolength.Trim().ToString() == "FT" )
19: return 3.28;
20: break;
21: case "FT":
22: if (tolength.Trim().ToString() == "MET" )
23: return 0.3048;
24: break;
25:
26: case "MILE":
27: if ( tolength.Trim ( ).ToString ( ) == "KM" )
28: return 1.609344;
29: break;
30:
31: case "KM":
32: if ( tolength.Trim ( ).ToString ( ) == "MILE" )
33: return 0.621371192;
34: break;
35: default:
36: return 0;
37:
38: }
39: return 0;
40: }
41:
42: }
43: }
新建ConvertTotalValue 类,代码如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4:
5: namespace MockTesting.Sample
6: {
7: public class ConvertTotalValue
8: {
9: private readonly IconversionService conversionService;
10:
11: public double ConvertTotal ( string fromlength, string tolength, double totvalue )
12: {
13: if ( fromlength.Trim ( ).Length <=0 || tolength.Trim ( ).Length <=0 )
14: return 0;
15: if ( totvalue == 0 )
16: return 0;
17: return conversionService.GetConversionRate ( fromlength, tolength ) * totvalue;
18: }
19:
20: public ConvertTotalValue ( IconversionService conversionService )
21: {
22: this.conversionService = conversionService;
23: }
24: }
25: }
说明:ConvertTotalValue 类里有一个只读的实例 IconversionService 接口,在其构造函数里实例化。那么这个实例就可以看作是一个 mock。
使用Nmock库单元测试 .NET 业务对象(比较)
下面,为了演示一般的单元测试(使用 NUnit 库)与模拟测试(使用 Nmock库 )的区别,在 ConversionServicesTest 类创建两个不同的测试方法。
在ShowCorrectLengthConversion 方法中,使用一个 ConversionServices类的对象,并调用GetConversionRate 方法获得转换系数。如果你 debug 一下 GetConversionRate方法,那么会发现程序的控制被转交给重构的GetConversionRate 函数。
而在ShowCorrectLengthConversionWithMock 方法中,没有创建任何 ConversionServices类的实例获得转换系数。取而代之的是真正的实现,创建一个 IconversionService 接口的mock实例,用来在计算转换的值。通过Nmock库函数创建一个模拟(mock)对象。请参考下面的代码:
1: Expect.Once.On ( mockconversionService ).
2: Method ( "GetConversionRate" ).
3: With ( objLengthType.FromLength, objLengthType.Tolength ).
4: Will ( Return.Value ( 3.28 ) );
Expect.Once.On将创建一个模拟(mock)实例。所以,调用参数为“MET”和“FT”的GetConversionRate方法时,会返回转换系数3.28。下面的代码显示完整的模拟测试代码:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4:
5: using NUnit.Framework;
6: using NMock2;
7:
8: namespace MockTesting.Sample
9: {
10: [TestFixture]
11: public class ConversionServicesTest
12:
13: {
14: private Mockery mocks ;
15:
16: private IconversionService mockconversionService ;
17:
18: private ConversionServices objConversionServices = new
19: ConversionServices ( );
20: private ConvertTotalValue objConvertTotalValue;
21:
22: [SetUp]
23: public void SetUp ( )
24:
25: {
26:
27: mocks = new Mockery ( );
28:
29: mockconversionService = mocks.NewMock<IconversionService> ( );
30:
31: objConvertTotalValue = new ConvertTotalValue ( mockconversionService );
32:
33: }
34: [Test]
35:
36: public void ShowCorrectLengthConversion ( )
37: {
38:
39: LengthType objLengthType = new LengthType ( );
40: objLengthType.FromLength= "MET";
41: objLengthType.Tolength= "FT";
42: Assert.AreEqual ( objConversionServices.GetConversionRate (
43: objLengthType.FromLength, objLengthType.Tolength ), 3.28 );
44:
45: }
46: [Test]
47: public void ShowCorrectLengthConversionWithMock ( )
48: {
49:
50: LengthType objLengthType = new LengthType ( );
51: objLengthType.FromLength= "MET";
52: objLengthType.Tolength= "FT";
53:
54: Expect.Once.On ( mockconversionService ).
55: Method ( "GetConversionRate" ).
56: With ( objLengthType.FromLength, objLengthType.Tolength ).
57: Will ( Return.Value ( 3.28 ) );
58:
59: Assert.AreEqual ( objConvertTotalValue.ConvertTotal ( objLengthType.FromLength,
60: objLengthType.Tolength, 10.0 ), 32.8 );
61: mocks.VerifyAllExpectationsHaveBeenMet ( );
62:
63: }
64:
65: }
66:
67: }
参考资料
http://www.nmock.org/http://www.devx.com/DevXNet/Article/45071
http://msdn.microsoft.com/en-us/magazine/cc163904.aspx