Rhino Mocks (RhinoMock)2

      本文将介绍一款在.Net平台下的Mock工具---Rhino Mocks 2,以及相关Mock工具的一些比较.在了解到Rhino Mocks 2之前我也接触了一些其他的Mock工具, 比如EasyMockJMockNMock NMock2,不过最终还是选择了Rhino Mocks 2, 相信你在看完本文的介绍后会和我做出同样的选择。(注: 本文不是Mock工具的入门文章,如果你之前尚未接触了解有关Mock对象,请先去了解相关资料)

本文由于编写时间较早, 现在RhinoMock有了新的变化,请参考 RhinoMock2 续一文.
      
从一个例子说起:

   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来完成我的测试.

下面是4Mock框架下的不同测试代码:  

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 }

       大致看来NMock2RhinoMocks2比较相像, 尤其在创建Mock对象的时候, 这点也是较之EasyMockNMock比较合理的地方, 因为你只需一个MockRepository就可以创建出多个Mock Object, 并且可以直接获得该类型的对象, 不需要再用mock.GetMock().这样的方法来获得所需的Mock对象.并且它们都利用using block增加方便性.(using block结束的时候会调用using对象的Dispose方法,此时将会自动调用mocks.VerifyAll(), 不需要象EasyMockNMock那样显式调用Verify方法.)

NMock2RhinoMocks2的区别主要在于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). 你可以看到RhinoMocks276行使用了mocks.ReplayAll(); NMock2并没有调用该方法. NMock2JMock都采用了Expectation的模型, 所有的期望发生的方法都使用Expect来定义.所以导致了它们之间的不同. RhinoMocks2就是结合两者的优点使得你既能获得类型安全又能使用类似Expect的简洁语法.

RhinoMocks2NMock2相比较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
[the parameter is a collection that contains this value]

List.IsIn(4)

One Of
[parameter equal to one of the objects in this list]

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
[perform regular expression validation]

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")


RhinoMocks2
的缺点:

对于所有的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.
---

你可能感兴趣的:(mock)