享受代码,享受人生

SOA is an integration solution. SOA is message oriented first.
The Key character of SOA is loosely coupled. SOA is enriched
by creating composite apps.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

RhinoMock2 续

Posted on 2006-05-05 21:10  idior  阅读(4181)  评论(7编辑  收藏  举报

在去年8月份我曾经写过两篇介绍RhinoMock的文章,最近有人在评论中指出在文章介绍的Mock对象的创建方式在新版本的RhinoMock中不再支持。由于我最近一直没有使用RhinoMock,于是我特地去查了一下有关资料,发现卢彦在去年12月份写的一篇文章中同样出现了这个问题,我赶紧到RhinoMock的讨论组查了一下资料,发现我之前的使用方法确实出了问题,该方法被Ayende cut掉了。为了避免我的文章继续"误人子弟", 特在本文中做一下解释,同时补充介绍一下RhinoMock的功能。

Disable Using Block

先简单介绍一下问题的背景,在BusinessReporter中使用了IEmailService的发送邮件的功能,为了测试BusinessReporter的Report方法,我们用到了Mock对象。具体内容详见Enterprise Test Driven Develop 一文。

    public interface IEmailService
    {
        bool Send(string message, string to);
    }
    public class BusinessReporter
    {
        public IEmailService emailService; // an external object.

        public BusinessReporter(IEmailService email)
        {
            this.emailService = email;
        }

        // method under test.
        public void Report()
        {
            //do something real
            string to = "idior"//just for test
            string message = "hello";
         
            bool result = emailService.Send(message, to);
            if (false == result)
                throw new EMailDeliveryException();      
        }
    }

问题出现在测试代码中:
   [Test]
   public void Report()
   {
       using (MockRepository repository = new MockRepository())
       {
           IEmailService emailSvc = repository.CreateMock<IEmailService>();
           BusinessReporter bizReporter = new BusinessReporter(emailSvc);
           Expect.Call(emailSvc.Send("hello", "idior")).Return(true);
           repository.ReplayAll();
           bizReporter.Report();
        }
    }

其中采取了using block的语法,看上去非常简洁。然而问题却恰恰出现在这个简洁的语法上,我们知道using block会被编译器解释为下面这个样子:

   [Test]
   public void Report()
   {
       MockRepository repository = null;
       try
       {
           repository = new MockRepository();
           IEmailService emailSvc = repository.CreateMock<IEmailService>();
           BusinessReporter bizReporter = new BusinessReporter(emailSvc);
           Expect.Call(emailSvc.Send("hello", "idior")).Return(true);
           repository.ReplayAll();
           bizReporter.Report();
       }
       finally
       {
           ((IDisposable)repository).Dispose();
       }
   }
        
再来看看不使用using block的方式:

    [Test]
    public void Report()
    {
         MockRepository repository = new MockRepository();
         IEmailService emailSvc = repository.CreateMock<IEmailService>();
         BusinessReporter bizReporter = new BusinessReporter(emailSvc);
         Expect.Call(emailSvc.Send("hello", "idior"));//.Return(true);
         repository.ReplayAll();
         bizReporter.Report();
         repository.VerifyAll();
     }

区别主要在于后者需要在测试方法的结尾手动调用repository.VerifyAll()方法。而该方法是每次使用Mcok对象都要调用的,所以通过using block的Dispose方法来自动调用自然是最方便而且不会被不小心遗漏。查看源代码你会发现Dispose方法中只有一行代码---this.VerifyAll();

但是,我们知道有很多东西当你正常使用的时候工作良好,一旦使用不当了却出现了意想不到的问题,这里的using block方式同样犯了这个毛病。

如果你注意观察以上两种方式时,你会发现他们的区别不仅仅在于后者手动调用repository.VerifyAll()方法, 前者还多出了一个try{ }finally{ } block, 而 ((IDisposable)repository).Dispose(); 正是处于finally block中,这就意味着不管在try block中的代码执行如何,VerifyAll方法始终要被调用到。而不使用using block方式则不会存在这个现象。
那么试着执行一下如下的代码,在设定emailSvc.Send("hello", "idior"));方法预期值的时候,我忘记了设置返回值:

    [Test]
    public void Report()
    {
        using (MockRepository repository = new MockRepository())
        {
            IEmailService emailSvc = repository.CreateMock<IEmailService>();
            BusinessReporter bizReporter = new BusinessReporter(emailSvc);
            Expect.Call(emailSvc.Send("hello", "idior"));//.Return(true);
            repository.ReplayAll();
            bizReporter.Report();
        }
    }

结果不出所料,出现了异常,但是异常显示的内容却是莫名其妙。
RhinoMockTest.Test.Report: System.InvalidOperationException : This action is invalid when the mock object is in record state.

如果不采用using block方式得到的异常信息则显得更为准确。
    [Test]
    public void Report()
    {
        MockRepository repository = new MockRepository();
        IEmailService emailSvc = repository.CreateMock<IEmailService>();
        BusinessReporter bizReporter = new BusinessReporter(emailSvc);
        Expect.Call(emailSvc.Send("hello", "idior")); //.Return(true);
        repository.ReplayAll();
        bizReporter.Report();
        repository.VerifyAll();          
    }
异常信息:
RhinoMockTest.Test.Report : System.InvalidOperationException : Previous method 'IEmailService.Send("hello", "idior");' require a return value or an exception to throw.

原因而在?问题就出在了finally关键字上,由于它的存在使得在前种方式下 repository.VerifyAll()方法始终将被执行。而一旦前面对Mock对象的使用出了问题,VerifyAll方法自然无法正确执行,它就会抛出新的异常,而把之前在使用中抛出的异常掩盖。如果在后者中没有finally关键字存在,一旦使用中出了异常,就直接抛出异常返回,而根本不会执行到 repository.VerifyAll()方法。

我想说到这里读者应该明白为何看上去挺美的using block方式会被RhinoMock所抛弃了。实际上早在去年12月份,RhinoMock2.5.4之后的版本中已经不再支持using block的使用。

下面针对RhinoMock在这段时间的版本更新做一些补充介绍。

Mocks, Dynamic Mocks and Partial Mocks

RhinoMock在目前的版本中存在三种类型的Mock对象,分别是Mock Object,Dynamic Mock和Partial Mock。在此简要介绍三种类型分别对应的应用场景。

首先介绍Mock Object和Dynamic Mock的区别:
在此为上面那个例子的EmailService增加一个验证email地址的方法,并在BusinessReporter发送Email前先利用该方法检查Email的合法性。

    public interface IEmailService
    {
        bool Send(string message, string to);
        bool ValidateAddress(string emailAddress);
    }
    public class BusinessReporter
    {
        public IEmailService emailService; // an external object.

        public BusinessReporter(IEmailService email)
        {
            this.emailService = email;
        }

        // method under test.
        public void Report()
        {
            //do something real
            string to = "idior"//just for test
            string message = "hello";
            emailService.ValidateAddress(to);
           
            bool result = emailService.Send(message, to);
            if (false == result)
                throw new Exception();
           
        }
    }

然后再次运行之前的测试方法:

    [Test]
    public void Report()
    {
        MockRepository repository = new MockRepository();
        IEmailService emailSvc = repository.CreateMock<IEmailService>();
        BusinessReporter bizReporter = new BusinessReporter(emailSvc);
        Expect.Call(emailSvc.Send("hello", "idior")).Return(true);
        repository.ReplayAll();
        bizReporter.Report();
        repository.VerifyAll();
    }

此时出现如下异常:
RhinoMockTest.Test.Report : Rhino.Mocks.Exceptions.ExpectationViolationException : IEmailService.ValidateAddress("idior"); Expected #0, Actual #1.

原因就是在实际的方法(bizReporter.Report())调用中触发到了被Mock对象的ValidateAddress("idior")方法,而在Mock对象的Expect阶段,我们并没有Expect到这个行为,所以出现了异常,可以把它称之为严格的回放( Strict replay )。这样做的目的就迫使你必须准确预计Mock对象的行为,从而获得更高的质量保障。但是某些情况下,开发者并不想关注Mock对象的所有行为,而只关注于Mock对象中某个特定的方法。这时就需要一种更灵活的Mock机制,为此RhinoMock提供了Dynamic Mock。把上面测试代码中的CreateMock换成DynamicMock后,测试就通过了。

    [Test]
    public void Report()
    {
        MockRepository repository = new MockRepository();
        IEmailService emailSvc = repository.DynamicMock<IEmailService>();
        BusinessReporter bizReporter = new BusinessReporter(emailSvc);
        Expect.Call(emailSvc.Send("hello", "idior")).Return(true);
        repository.ReplayAll();
        bizReporter.Report();
        repository.VerifyAll();
    }

不过值得注意的是不管是Mock Object还是Dynamic Mock,所有在Expect阶段设定的行为都必须被触发,否则都会导致测试无法通过。只不过当Mock对象中没有预期到的行为发生时Dynamic Mock会睁一只眼闭一只眼。
通常我们在Expect阶段设定的都是将要发生的方法,但是有时候我们想确定某个方法不被调用到。在Mock Object中,你必须预计所有将被调用到的方法,所以没有预计到的方法就肯定不会调用到。但是在Dynamic Mock中,你就无法确定某个方法没有被调用到了。针对这种情况RhinoMock也提供了解决方法。

你可以通过下面的方法保证view.Ask方法不被调用

    Expect.Call(view.Ask(null,null)).IgnoreArguments().Repeat.Never();

如前所述这是专门针对Dynamic Mock的,所以在Mock Object中没有必要使用该方法。

利用Mock Object和Dynamic Mock,我们已经可以很好的完成对Interface的Mcok。而Partial Mock则是专门针对class的Mock对象,通常被用于Template Method模式,比如下面这个例子。

    public abstract class Employee
    {
        private decimal baseSalary=1000;
        public virtual decimal CaculateSalary()
        {
            return baseSalary * GetRank();
        }

        public abstract int GetRank();
    }

此时希望对Emplyee做Mock,虽然其中有两个虚方法,但是我只想Mock其中的GetRank方法,这时Partial Mock就派上用场了。

    [Test]
    public void CaculateSalary()
    {
        MockRepository repository = new MockRepository();
        Employee manager = repository.PartialMock<Employee>();
        Expect.Call(manager.GetRank()).Return(2);
        repository.ReplayAll();

        Assert.AreEqual(2000,manager.CaculateSalary());

        repository.VerifyAll();
    }

在Expect阶段我们只设定了GetRank方法,所以在调用另一个虚方法CaculateSalary的时候,仍旧使用原来的方法定义。

此时如果使用Mock Object,会抛出如下异常:
RhinoMockTest.Test.CaculateSalary : Rhino.Mocks.Exceptions.ExpectationViolationException : Employee.CaculateSalary(); Expected #0, Actual #1.

而使用Dynamic Mock同样引发异常:
RhinoMockTest.Test.CaculateSalary :  expected: <2000>   but was: <0>


相关资料:

RhinoMock官方文档
RhinoMock2
Enterprise Test Driven Develop