模拟入门
本文将解释模拟及其在单元测试中带来的各种好处。
引言
模拟是单元测试不可或缺的一部分。虽然你可以在不使用模拟的情况下运行单元测试,但这会极大地降低单元测试的执行速度,并且会依赖于外部资源。本文将解释模拟及其在单元测试中带来的各种好处。
现实世界中的例子
我们将专注于一个现实世界的例子,以便你更好地理解模拟。假设我们正在开发一个银行软件,它从某个数据库中提取客户余额信息。该软件还会向外部网络服务发出身份验证请求。身份验证过程很慢,因为它涉及许多不同的步骤,包括发现网络服务、序列化输入和输出参数等。
我们想测试返回的客户余额是否正确。
这是带有`Authenticate`方法的网络服务
public class AverageJoeBankService : IAverageJoeBankService
{
public bool Authenticate(string userName, string password)
{
// This is just simulate the time taken for the web service to authenticate!
System.Threading.Thread.Sleep(5000);
return true;
}
}
如你所见,我们只是模拟了`Authenticate`方法。我们假设验证用户至少需要5秒钟。当然,在实际应用中,你的`Authenticate`方法将与真实的网络服务通信。
这是`AccountBalanceService`方法
public class AccountBalanceService
{
private IAverageJoeBankService _averageJoeService;
public AccountBalanceService(IAverageJoeBankService averageJoeService)
{
_averageJoeService = averageJoeService;
}
public double GetAccountBalanceByUser(User user)
{
// the authenticate method below takes too much time!
bool isAuthenticated = _averageJoeService.Authenticate(user.UserName,
user.Password);
if (!isAuthenticated)
throw new SecurityException("User is not authenticated");
// access database using username and get the balance
return 100;
}
}
这是我们第一次尝试编写单元测试
[Test]
public void should_be_able_to_get_the_balance_successfully_without_using_mock_objects()
{
User user = new User();
user.UserName = "johndoe";
user.Password = "johnpassword";
_accountBalanceService = new AccountBalanceService(new AverageJoeBankService());
Assert.AreEqual(100, _accountBalanceService.GetAccountBalanceByUser(user));
}
在上面的单元测试中,我们只是使用了`AccountBalanceService`类的具体实现,它会触发实际的身份验证方法。
运行测试时,你会得到以下结果

测试通过了,你脸上露出了灿烂的笑容。别高兴得太早!看看运行测试所花费的时间:6.69 秒。对于单个测试来说,这时间太长了。除了时间问题外,测试还存在其他问题。测试依赖于`AverageJoeBankService`。如果网络服务不可用,则测试将失败。单元测试应该是独立的,并且应该运行速度很快。让我们通过引入模拟对象来加快测试速度。
哦,等等!我们还没有解释模拟对象在单元测试中的含义。模拟对象就像真实对象,但它们什么也不做。我们知道我们刚刚给你的定义有点疯狂,但你很快就会明白我们的意思。
这是使用模拟对象的单元测试
private AccountBalanceService _accountBalanceService;
private MockRepository _mocks;
[SetUp]
public void initialize()
{
_mocks = new MockRepository();
}
[Test]
public void should_be_able_to_get_the_balance_successfully()
{
User user = new User();
user.UserName = "JohnDoe";
user.Password = "JohnPassword";
var averageJoeService = _mocks.DynamicMock<IAverageJoeBankService>();
_accountBalanceService = new AccountBalanceService(averageJoeService);
using (_mocks.Record())
{
SetupResult.For(averageJoeService.Authenticate(null,
null)).IgnoreArguments().Return(true);
}
using (_mocks.Playback())
{
Assert.AreEqual(100,_accountBalanceService.GetAccountBalanceByUser(user));
}
}
首先,我们创建了`MockRepository`,它是Rhino Mocks框架的一部分。你可能会说“Rhino Mocks是什么鬼?”Rhino mock是一个模拟框架,它为你提供不同的模拟功能以及模拟对象的方法。还有其他几个模拟框架,包括NMock、NMock2、TypeMock、MoQ。
无论如何,在上面的单元测试中,我们使用以下代码创建了一个模拟对象
var averageJoeService = _mocks.DynamicMock<IAverageJoeBankService>();
_accountBalanceService = new AccountBalanceService(averageJoeService);
将模拟的`AverageJoeService`(不是真正的服务)传递给`AccountBalanceService`后,我们设置了我们的期望。
using (_mocks.Record())
{
SetupResult.For(averageJoeService.Authenticate(null,
null)).IgnoreArguments().Return(true);
}
这意味着当`averageJoeService.Authenticate`方法被触发时,返回true,这样我就可以继续执行了。你还可以看到我们并不关心传入的参数,这就是为什么我们使用`IgnoreArguments`。
下一部分是回放,这是将触发期望并产生预期结果的代码。这是回放部分
using (_mocks.Playback())
{
Assert.AreEqual(100,_accountBalanceService.GetAccountBalanceByUser(user));
}
这是单元测试的输出

如你所见,现在单元测试只需要1.81秒。测试不仅更快,而且现在也不依赖于`AverageJoeBankService`。这意味着即使网络服务宕机,你也能运行上面的测试。
结论
在本文中,我们介绍了模拟背后的概念。模拟通过消除外部依赖性来帮助改进单元测试,从而产生更好、更快、更独立的单元测试。
希望你喜欢这篇文章,编程愉快!
注意:我们还为此主题创建了一个屏幕录制视频,你可以使用以下链接观看:屏幕录制:模拟入门