在去年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