本文将介绍一款在.Net平台下的Mock工具---Rhino Mocks 2,以及相关Mock工具的一些比较.在了解到Rhino Mocks 2之前我也接触了一些其他的Mock工具, 比如EasyMock,JMock,NMock, NMock2,不过最终还是选择了Rhino Mocks 2, 相信你在看完本文的介绍后会和我做出同样的选择。(注: 本文不是Mock工具的入门文章,如果你之前尚未接触了解有关Mock对象,请先去了解相关资料)
27 public interface ISubscriber
28 {
29 int MultiplyTwo(int i);
30 void Receive(Object message);
31 }
32
33 public class Publisher
34 {
35 private List<ISubscriber> subscribers = new List<ISubscriber>();
36
37 public void Add(ISubscriber s)
38 {
39 subscribers.Add(s);
40 }
41
42 public void Publish(object msg)
43 {
44 subscribers.ForEach(delegate(ISubscriber s) { s.Receive(msg); });
45 }
46 public int Caculate(int i)
47 {
48 int result = 0;
49 subscribers.ForEach(delegate(ISubscriber s) { result += s.MultiplyTwo(i); });
50 return result;
51 }
52 }
以上是一个Observer模式的小例子, 为了更加全面的体现出Mock对象的特性, 我在ISubscriber加了一个有返回值的方法MultiplyTwo, 它所做的事情就是将参数乘2并返回.
现在我们将对Publisher进行测试, 然而Publisher中涉及到了另一个对象ISubscriber. 而我们暂时还没实现ISubscriber , 所以我们将利用Mock Object来完成我的测试.
下面是4种Mock框架下的不同测试代码:
EasyMock.Net
55 namespace EasyMockTest
56 {
57 [TestFixture]
58 public class PublisherTest
59 {
60 [Test]
61 public void OneSubscriberReceivesAMessage()
62 {
63 //setup
64 MockControl mockCtrl= MockControl.CreateControl(typeof(ISubscriber));
65 ISubscriber subMock = mockCtrl.GetMock() as ISubscriber;
66 Publisher publisher = new Publisher();
67 publisher.Add(subMock);
68 object message = new object();
69
70 //record
71 mockCtrl.Reset();
72 subMock.Receive(message);
73 subMock.MultiplyTwo(5);
74 mockCtrl.SetReturnValue(10);
75 mockCtrl.Replay();
76
77 //execute
78 publisher.Publish(message);
79 Assert.AreEqual(10, publisher.Caculate(5));
80
81 //verify
82 mockCtrl.Verify();
83 }
84 }
85 }
NMock
55 namespace NMockTest
56 {
57 [TestFixture]
58 public class PublisherTest
59 {
60 [Test]
61 public void OneSubscriberReceivesAMessage()
62 {
63 // set up
64 Mock mockSubscriber = new DynamicMock(typeof(ISubscriber));
65 Publisher publisher = new Publisher();
66 publisher.Add((ISubscriber)mockSubscriber.MockInstance);
67 object message = new Object();
68
69 // expectations
70 mockSubscriber.Expect("Receive", message); //commentted is still ok
71 mockSubscriber.ExpectAndReturn("MultiplyTwo", 10, 5);
72
73 // execute
74 publisher.Publish(message);
75 Assert.AreEqual(10, publisher.Caculate(5));
76
77 // verify
78 mockSubscriber.Verify();
79 }
80 }
81 }
NMock2
55 namespace NMock2Test
56 {
57 [TestFixture]
58 public class PublisherTest
59 {
60 [Test]
61 public void OneSubscriberReceivesAMessage()
62 {
63 using (Mockery mocks = new Mockery())
64 {
65 //setup
66 ISubscriber subMock = mocks.NewMock(typeof(ISubscriber)) as ISubscriber;
67 Publisher publisher = new Publisher();
68 publisher.Add(subMock);
69 object message = new object();
70
71 //expectations
72 Expect.Once.On(subMock).Method("Receive").With(message);
73 Expect.Once.On(subMock).Method("MultiplyTwo").With(5).Will(Return.Value(10));
74
75 //excute
76 publisher.Publish(message);
77 Assert.AreEqual(10, publisher.Caculate(5));
78 }// verify when mocks dispose
79 }
80 }
81 }
RhinoMocks2
55 namespace RhinoMocks2Test
56 {
57 [TestFixture]
58 public class PublisherTest
59 {
60 [Test]
61 public void OneSubscriberReceivesAMessage()
62 {
63 using (MockRepository mocks = new MockRepository())
64 {
65 //setup
66 ISubscriber subMock = mocks.CreateMock(typeof(ISubscriber)) as ISubscriber;
67 Publisher publisher = new Publisher();
68 publisher.Add(subMock);
69 object message = new object();
70
71 //record with expectation model
72 subMock.Receive(message);
73 Expect.On(subMock).Call(subMock.MultiplyTwo(5)).Return(10);
74
75 //end record
76 mocks.ReplayAll();
77
78 //excute
79 publisher.Publish(message);
80 Assert.AreEqual(10, publisher.Caculate(5));
81 }//verify when mocks dispose
82 }
83 }
84 }
大致看来NMock2和RhinoMocks2比较相像, 尤其在创建Mock对象的时候, 这点也是较之EasyMock和NMock比较合理的地方, 因为你只需一个MockRepository就可以创建出多个Mock Object, 并且可以直接获得该类型的对象, 不需要再用mock.GetMock().这样的方法来获得所需的Mock对象.并且它们都利用using block增加方便性.(在using block结束的时候会调用using对象的Dispose方法,此时将会自动调用mocks.VerifyAll(), 不需要象EasyMock和NMock那样显式调用Verify方法.)
而NMock2和RhinoMocks2的区别主要在于Expectation阶段.
仅仅从语法上来看, 你会发现它们都使用了Expectation的语法, 但是RhinoMocks2显然更胜一筹.
|
void Receive(Object message);
|
NMock2
|
Expect.Once.On(subMock).Method("Receive").With(message);
|
RhinoMocks2
|
subMock.Receive(message);
|
RhinoMocks2的语法非常简洁也更加自然. 不过如果之前一直使用Expectation语法的可能会觉得奇怪, 怎么把方法的执行放到了Expectation阶段. 注意到RhinoMocks2的这个特点,你就会觉得很自然了, 对于没有返回值的方法RhinoMocks2是这样处理的.
mock.Method(Parameter p);
LastCall.On(mock);
不过LastCall.On(mock);可以被省略.RhinoMocks2会自动为你补上.
再来看看对于有返回值的方法的处理:
|
int MultiplyTwo(int i);
|
NMock2
|
Expect.Once.On(subMock).Method("MultiplyTwo").With(5).Will(Return.Value(10));
|
RhinoMocks2
|
Expect.On(subMock).Call(subMock.MultiplyTwo(5)).Return(10);
|
简而言之,RhinoMocks2是类型安全的. NMock2中使用的是字符串型的方法名,这样既没有了IDE自动完成的支持,而且要在测试运行时才能检查出错误. 并且对于参数和返回值的语法也是RhinoMocks2处理的更加简洁,自然.
为什么NMock2甚至JMock没有使用类型安全的语法? 不是它们没有想到,而是由于它们和RhinoMocks2采取的实现模型是不一样的. EasyMock.Net 和RhinoMocks2采用的是Record/Replay的模型,即先记录Record将会发生的事, 然后在回放(Replay). 你可以看到RhinoMocks2的76行使用了mocks.ReplayAll(); 而NMock2并没有调用该方法. NMock2和JMock都采用了Expectation的模型, 所有的期望发生的方法都使用Expect来定义.所以导致了它们之间的不同. 而RhinoMocks2就是结合两者的优点使得你既能获得类型安全又能使用类似Expect的简洁语法.
RhinoMocks2和NMock2相比较NMock都学习了JMock强大的Constraints.
Constraint:
|
Example:
|
|
Object
|
Anything
|
Is.Anything()
|
Equal
|
Is.Equal(3)
|
|
Not Equal
|
Is.NotEqual(3) or !Is.Equal(3)
|
|
Null
|
Is.Null()
|
|
Not Null
|
Is.NotNull()
|
|
Type Of
|
Is.TypeOf(typeof(string))
|
|
Greater Than
|
Is.GreaterThan(10)
|
|
Greater Than Or Equal
|
Is.GreaterThanOrEqual(10)
|
|
Less Than
|
Is.LessThan(10)
|
|
Less Than Or Eqaul
|
Is.LessThanOrEqual(10)
|
|
Property
|
Equal To Value
|
Property.Value("Length",0)
|
Null
|
Property.IsNull("InnerException")
|
|
Not Null
|
Property.IsNotNull("InnerException")
|
|
List
|
Is In List |
List.IsIn(4)
|
One Of |
List.OneOf(new int[]{3,4,5})
|
|
Equal
|
List.Equal(new int[]{4,5,6})
|
|
Text
|
Starts With
|
Text.StartsWith("Hello")
|
Ends With
|
Text.EndsWith("World")
|
|
Contains
|
Text.Contains("or")
|
|
Like |
Text.Like("Rhino|rhino|Rhinoceros|rhinoceros" )
|
|
Operator Overloading
|
And - operator &
|
Text.StartsWith("Hello") & Text.Contains("bar")
|
Or - operator |
|
Text.StartsWith("Hello") & Text.Contains("bar")
|
|
Not - operator !
|
!Text.StartsWith("Hello")
|
对于所有的mock对象, 你必须预计(Expect)到它在执行时(excute)的所有将发生方法, 否则都会导致测试无法通过, 唯一例外的一点就是NMock对此没有要求.当你把NMockTest的第70行注释掉,测试依然通过.
不知道是不是因为这样的原因RhinoMocks2并没有提供Expect.Never.On这样的语法. 也就是我无法保证某个方法不被调用. 而其他的框架都实现了类似的功能.
我的期望:
Expect.On(subMock).Call(subMock.MultiplyTwo(5)).Return(10);
其中On方法似乎显的多余.不知道能不能改成下面这个样子.
Expect.Call(subMock.MultiplyTwo(5)).Return(10);
甚至于变成这样
subMock.MultiplyTwo(5)==10;
当然Expectation的模型应该保留,因为对于某些需要指定异常约束的情况你是无法通过subMock.MultiplyTwo(5)==10;这样的形式来描述的.
虽然Expect的语法的语义比较强, 但是在书写的时候还是比较麻烦.不知道能不能有更好的模型.
Mock对象在测试时无法调试, 这点和NUnit那样基于状态的测试相比差了点, 只能靠自己动脑子了.
参考资料: RhinoMocks2 Enterprise Test Driven Develop
后记: 在询问过Rhino Mocks 2的作者Ayende Rahien之后,对于我的期望,他给了以下回复,看来在技术不太可行。
--- Question
hi, recently i came across RhinoMocks2.0. i have got a question
Expect.On(mock).Call(mock.MethodA(1)).Return(2);
why we still need On(Mock) here?
and i want to know if we can let the expect model be like this.
mock.MethodA(1)==2;
---
--- Reply
The On(mock) so the repository would know which mock object the call
originated from, the Call() syntax is a merely a better syntax for this. The
problem is that there really is no way to implement a way to detect what was
the last call in a good way across multiple mock objects and multiply mock
repositories.
About the second question, currently I can think of no way to do that, since
this would require cooperation from the compiler itself in order to discover
what the value of 2 is.
Consider the behind the scenes implications of this, you first record an
action, and the return value is stored in anticipation for the action's
replay.
How would this syntax allow the framework to grab the return value and so
return it when called ?
---
---Question
I wonder why don't u implement Expect.Never.On,or i haven't found it?
NMock2 and JMock both have some similar function.
---
---Reply
That is the default behavior.
The idea is that /any/ call that was not explicitly setup as expected is not
expected.
You can do something similar by:
Expect.On(mock).Call(mock.Method()).Repeat.Times(0,0);
This is not neccecary, however and can cause problems if you're using
Ordered behaviors.
---