NMock是使用于.NET的一个动态模拟对象库,它可以在某单一组件的依赖组件并不一定实现的情况下对某单一组件进行测试。这意味着我们可以仅测试一个类,而不是一整组对象,并且可以更清晰的找到微小的BUG,Mock对象一般应用在TDD(测试驱动开发)过程中。
一个Mock对象:
*使用接口呈现对象,在测试中替代真实的对象。
*允许定义预期(Expectations),详细的制定Mock对象与被测试对象预期的交互.
*如果任何一个预期被违反,那么测试将会失败.
*同时也可以象存根(Stub)一样,允许在测试过程中从指定的对象通过模拟方法返回值。
我将用一个Sample演示如何使用Mock对象,在演示中我将使用NUnit编写单元测试,并尝试用TDD的方式来实现目标,简单的来说就是我会先写测试,设计预期,而具体的实现将会以通过测试为目标来编写.为了便与应用,我的演示基于MVP模式.
演示模拟一个非常简单的转账页面,用户在转账页面输入转入账号,转出账号与金额,进行转账操作,同时我们还要考虑汇率.
首先,我们有一个名为TransferFunds的页面,按照MVP的模式,将会有一个名为ITransferFundsView的View,以及一个名为TransferFundsPresenter的Presenter.因为用户需要输入转入账号,转出账号与金额,所以ITransferFundsView看起来应该是这样:
{
string From { get; }
string To { get; }
double Amount { get; }
}
用户填写完以上信息后点击按钮,按钮将调用Presenter的一个TranserClicked方法进行转账,现在我们编写第一个测试.
public void TransferClickedTest()
{
var mockery = new Mockery();
var view = mockery.NewMock<ITransferFundsView>();
var accountService = mockery.NewMock<IAccountService>();
var presenter = new TransferFundsPresenter
{
View = view,
AccountService = accountService
};
Expect.Once.On(view).GetProperty("From").Will(Return.Value("purple"));
Expect.Once.On(view).GetProperty("To").Will(Return.Value("ento"));
Expect.Once.On(view).GetProperty("Amount").Will(Return.Value(100.00));
Expect.Once.On(accountService).Method("TransferFunds").With("purple", "ento", 100.00);
presenter.TransferClicked();
mockery.VerifyAllExpectationsHaveBeenMet();
}
按照以上编写的测试(仅仅是这个测试,里面的接口并没有完全实现),presenter应该拥有一个注入的IAccountService接口,presenter的方法TransferClicked调用IAccountService的TransferFunds方法并传入3个参数.下面进行详细解释.
var view = mockery.NewMock<ITransferFundsView>();
var accountService = mockery.NewMock<IAccountService>();
var presenter = new TransferFundsPresenter
{
View = view,
AccountService = accountService
};
以上是初始化操作,view accountService都是模拟的对象,并用两个对象初始化peresenter.这里的设计表示presenter应该包含一个IAccountService接口.
Expect.Once.On(view).GetProperty("To").Will(Return.Value("ento"));
Expect.Once.On(view).GetProperty("Amount").Will(Return.Value(100.00));
Expect.Once.On(accountService).Method("TransferFunds").With("purple", "ento", 100.00);
设计预期,我们设定,View的三个属性,必须将被至少调用一次,当调用View对象的3个属性的时候,分别返回3个指定的值.同时,我们设定accountService的方法TransferFunds方法将至少被调用一次,而且这个方法将传入3个参数,注意,这3个参数与View对象返回的值一样.这表示我们设定传入的3个参数必须等于View的3个属性.
接下来调用presenter的TransferClicked方法,这个方法是被测试的方法.然后使用mock判断所有的预期是否被碰到.
现在运行测试?失败,因为我们还没有IAccountService接口,所以,我们现在要创建IAccountService接口.因为我们还设定了需要有一个名为TransferFunds的方法,所以IAccountService接口看起来应该是这样.
{
void TransferFunds(string From, string To, double Amount);
}
然后我们要实现presenter.TransferClicked方法的内部,根据设定,我们将要使用View的3个参数,并且将他们传给IAccountService的方法.所以为了通过测试,TransferClicked方法看起来应该是这样:
{
var from = View.From;
var to = View.To;
var amount = View.Amount;
AccountService.TransferFunds(from, to, amount);
}
现在运行测试…成功通过…OK,我们现在想给这个方法设计一点新的东西,比如当用户输入一个错误的值,我们该提示用户,现在我们要修改一下原来的测试,添加一个新的预期,现在预期变成这样
Stub.On(view).GetProperty("To").Will(Return.Value("BAD WORD"));
Stub.On(view).GetProperty("Amount").Will(Return.Value(100.00));
Expect.Once.On(view).SetProperty("DisplayErrorMessage").To(true);
Expect.Once.On(accountService).Method("TransferFunds").
With("purple","BAD WORD",100.00).Will(Throw.Exception(new Exception()));
新增了两个预期,我们设定View 的一个属性,将被赋值为true,另一个预期是,当accountService.TransferFunds的3个参数为"purple","BAD WORD",100.00,将抛出一个异常,这里我没有使用Expect定义预期,使用了Stub,Stub跟Expect的唯一不同在于,Stub的定 义的预期可以被交互0次或者N次,如果Stub定义的预期没有被交互,测试将继续运行,而不会出错.
为了通过这个测试,我们必须修改View以及Presenter的方法,修改后的方法如下:
{
string From { get; }
string To { get; }
double Amount { get; }
bool DisplayErrorMessage { set; }
}
public void TransferClicked()
{
var from = View.From;
var to = View.To;
var amount = View.Amount;
try
{
AccountService.TransferFunds(from, to, amount);
}
catch (Exception)
{
View.DisplayErrorMessage = true;
}
}
现在,我们通过了测试,NMock经常使用的功能基本上都出现在了上面,也许这个实例过于简单,我并不是想去演示如何测试一个复杂的方法,只是通过这个演示 介绍NMock的常用属性,它们将如何使用,同时也是尝试如何用测试驱动去实现一个方法,先写出测试,把目标定位在通过测试的基础上去实现功能,写测试其 实也就是在设计方法.
NMock的预期是对过程的设计,在具体的实现里还需要使用NUnit的断言对结果进行验证.
水平有限,欢迎指正