单元测试中,为了让单元测试程序完全脱离外部依赖,需要使用到Mock对象和Stub对象。虽然可以手工编写Mock对象和Stub对象,但通常我们都使用Mocking Framework来帮助我们简单快速的构建需要的Mock对象以及Stub对象。
一、概述
常见的Mocking Framework有如下几种:
1、Rhino Mocks V3.6(2009-9-1)
Rhino Mocks是由Ayende Rahien 开发的一个开源项目,目前版本支持.NET 3.5 & 4.0以及Silverlight的CLR, 采用Castle DynamicProxy方式实现Mock对象的构建。
采用Lambda表达式和泛型以及扩展方法实现调用API的强类型化。
因为实现方式的限制,只能对Interface以及可继承的类进行Mock处理,对于sealed类、非virtual,非抽象以及静态方法则无能为力。
因为考虑与旧版本的兼容性的问题, API较为繁琐,冗余方法较多,学习使用时容易混淆。
被Mvccontrib项目所支持,可以被该项目的TestHelper调用,并用于ASP.NET中Stub对象的模拟。
轻量级应用,不需要被安装,只需要在测试项目中进行引用即可实现对其的调用。
public class BrainTests
{
/// <summary>
/// Verify that if hand throws an exception having touched a hot iron, <see cref="IMouth.Yell"/> gets called.
/// </summary>
/// <remarks>
/// Rhino Mocks can mock both interfaces and classes - however, only virtual methods
/// of a class can be mocked (try changing IHand/IMouth to Hand/Mouth).
/// </remarks>
[Test]
public void TouchHotIron_Yell()
{
var hand = MockRepository.GenerateStub<IHand>();
var mouth = MockRepository.GenerateMock<IMouth>();
hand.Stub(h => h.TouchIron(null)).Constraints(Is.Matching<Iron>(i => i.IsHot)).Throw(new BurnException());
mouth.Expect(m => m.Yell());
var brain = new Brain(hand, mouth);
brain.TouchIron(new Iron { IsHot = true });
mouth.VerifyAllExpectations();
}
}
Moq是一个比较新的开源项目,最早提出了用Lambda表达式和泛型以及扩展方法实现调用API的强类型化,不用通过字符串方式描述Mock对象的方法以及属性,这个特点已经被其他Mocking Framework迅速吸收;
Moq的语法及其简单,学习成本较低,也是最早提出隐性Record/Reply模式的Mocking Framework.
Moq跟Rhino Mocks一样是采用Castle DynamicProxy方式实现Mock对象的构建,因此也无法对sealed类、非Virtual, 非抽象以及静态方法进行mock操作。
被Mvccontrib项目所支持,可以被该项目的TestHelper调用,并用于ASP.NET中Stub对象的模拟。
public class BrainTests
{
/// <summary>
/// Verify that if hand throws an exception having touched a hot iron, <see cref="IMouth.Yell"/> gets called.
/// </summary>
/// <remarks>
/// Moq can mock both interfaces and classes - however, only virtual methods
/// of a class can be mocked (try changing IHand/IMouth to Hand/Mouth).
/// </remarks>
[Test]
public void TouchHotIron_Yell()
{
var hand = new Mock<IHand>();
var mouth = new Mock<IMouth>();
hand.Setup(x => x.TouchIron(HotIron)).Throws(new BurnException());
var brain = new Brain(hand.Object, mouth.Object);
brain.TouchIron(new Iron { IsHot = true });
mouth.Verify(x => x.Yell());
}
/// <summary>
/// Parameter expectations tend to be quite verbose in Moq, so we provide a custom matcher.
/// This needs a matcher method and a bool sibling method for evaluating the expectations.
/// Calling this matcher is technically equivalent to <code>It.Is{Iron}(i => i.IsHot)</code>.
/// </summary>
public Iron HotIron
{
get { return Match<Iron>.Create(x => x.IsHot); }
}
}
3、Typemock 2010 商业软件,有试用版
Typemock的实现方式比较特殊,它通过调用.NET Profiler接口,在代码运行时进行拦截,注入重定向代码,从而实现Mock处理。
因为实现方式上的截然不同,也给予了Typemock更为强大的能力:可以Mock私有类,Sealed类以及非Virtual、非抽象甚至静态方法。
也可以对mscorlib里定义的基本类型进行Mock,例如DateTime.Now。
在使用API接口上,Typemock基本与Moq很类似,也是非常简单易用。
使用Ivonna实现对ASP.NET环境下Stub对象的模拟。
必须安装后集成到Visual Studio开发环境中才能使用,并且因为实现机制的原因,执行速度较慢。
public class BrainTests
{
/// <summary>
/// Can mock both classes and interfaces, can mock private/static classes etc.
/// </summary>
[Test, Isolated]
public void TouchHotIron_Yell()
{
var hand = Isolate.Fake.Instance<Hand>();
var mouth = Isolate.Fake.Instance<Mouth>();
var iron = new Iron { IsHot = true };
Isolate.WhenCalled(() => hand.TouchIron(iron)).WillThrow(new BurnException());
var brain = new Brain(hand, mouth);
brain.TouchIron(iron);
Isolate.Verify.WasCalledWithAnyArguments(() => mouth.Yell());
}
/// <summary>
/// Can mock objects WITHOUT DEPENDENCY INJECTION.
/// </summary>
[Test, Isolated]
public void TouchHotIron_Yell_NoDependencyInjection()
{
var hand = Isolate.Fake.Instance<Hand>();
var mouth = Isolate.Fake.Instance<Mouth>();
Isolate.Swap.NextInstance<Hand>().With(hand);
Isolate.Swap.NextInstance<Mouth>().With(mouth);
var iron = new Iron { IsHot = true };
Isolate.WhenCalled(() => hand.TouchIron(iron)).WillThrow(new BurnException());
//notice we're not passing the mocked objects in.
var brain = new Brain();
brain.TouchIron(iron);
Isolate.Verify.WasCalledWithAnyArguments(() => mouth.Yell());
}
}
4、Moles V0.94.51006.1(2010-10-12)
微软Research项目中的一员,非开源但免费使用,目前还未能发布正式版,在稳定性上稍微欠缺。
实现方式也是非常特殊:通过MSBuild的API,在编码编译时甚至编译后对Assembly进行重写,从而在已有的IL代码中插入重定向代码,从而实现Mock处理。
Moles和Typemock一样,也是基本可以Mock所有的类和方法,甚至私有类,私有方法,也可以对mscorlib里定义的基本类型进行Mock。
被处理过后的Assembly中直接生成了一些相应的Mock对象,因此Moles的API完全和其他Mocking Framework截然不同,而是采用约定的命名空间和首字母方式进行调用,给人感觉API比较怪异。
Moles需要安装并集成到Visual Studio中,并且在安装过程中对.NET Framework中很多类库也进行了特殊处理,进而也可以引用相应的类库和方法实现对ASP.NET环境的支持。
二、横向比较
Rhino Mocks 3.6 |
Moq 4.0.10827 |
Typemock 2010 |
Moles 0.94.51006.1 |
||
实现方式 |
Castle Dynamic Proxy |
Castle Dynamic Proxy |
.NET Profiler |
MS Build |
|
授权情况 |
开源/免费 |
开源/免费 |
不开源/不免费 |
不开源/免费 |
|
强类型支持 |
是 |
是 |
是 |
是 |
|
递归Mocks对象 |
支持 |
支持 |
支持 |
支持 |
|
Partial Mocks |
支持 |
支持 |
支持 |
支持 |
|
Virtual Method |
支持 |
支持 |
支持 |
支持 |
|
Abstract Method |
支持 |
支持 |
支持 |
支持 |
|
Public Class |
支持 |
支持 |
支持 |
支持 |
|
Interface |
支持 |
支持 |
支持 |
支持 |
|
Sealed Class |
不支持 |
不支持 |
支持 |
支持 |
|
Non-Abstract Method |
不支持 |
不支持 |
支持 |
支持 |
|
Non-Virtual Method |
不支持 |
不支持 |
支持 |
支持 |
|
Static Method |
不支持 |
不支持 |
支持 |
支持 |
|
语法结构 |
一般 |
简炼 |
简炼 |
较差 |
|
对依赖注入框架的依赖性 |
依赖 |
依赖 |
不依赖 |
不依赖 |
|
对ASP.NET支持 |
MVC Contrib |
MVC Contrib |
Ivonna |
自带 |
三、总结
作为商业软件的Typemock确实除了在执行效率和安装使用方面存在一定的缺点外,基本是最为强大的Mocking Framework;
而再看开源的Rhino Mocks和Moq基本非常接近,Moq在语法上更简炼易用一些(PS, 据说RM 4.0会大幅改进API),一般情况下这两个开源的Mocking Framework都应该可以满足需求,但他们都高度依赖于依赖注入框架,例如Unity, Autofac等等。不过关于这点,是优势还是劣势社区里也众说纷纭。不少支持者认为,正因为这样,强迫项目必须实现依赖倒置,从而降低了项目的耦合性,以达到较高的可测试性以及可维护性。而Typemock反而因为功能太过强大,无需依靠IOC容器反而受到开源社区的一些批评。
最后再来看新秀Moles,这个东西从语法以及设计上都算是个另类,基本和其他框架没有相同之处,但功能上却也非常强大。不过从接口设计方面看,确实比较糟糕,不够优雅。另外,Moles有一个比较致命的弱点:因为Moles基于对程序集的编译后静态织入,因此,若被单元测试的程序集发生改变之后,必须整个重新运行编译再运行单元测试,这个在实际使用中是一个非常麻烦的事情。
因此,在我看来,在项目实践中,若采用Typemock这个商业框架,则依然可以强制要求项目依赖于Ioc容器进行开发,保证不滥用Typemock的强大功能,只在不得不使用Typemock对mscorlib以及静态,私有等特殊方法进行mock时才使用。
若采用免费方案,也许应该采用Moq为主,Moles为辅的方式进行:即使用Moq进行大部分的Mock操作,只有遇到当Moq无法解决的场景再使用Moles进行处理。
注:文章中示例代码详见:mocking-frameworks-compare,该项目有详细的代码对比