如何在测试中使用模拟框架





5.00/5 (3投票s)
何时可以使用,以及何时不应该使用模拟框架
让我们来探讨何时可以使用,以及何时不应该使用模拟框架。同时,我也会展示一些在使用模拟框架时可能用到的模式。
如果你并不在意...
有时,你想要测试的类(被测系统,SUT)需要一些依赖项。但可能你的测试实际上并不依赖于这些依赖项的工作。如果你对依赖项的所有成员返回的默认值感到满意,你可以使用模拟框架来创建它。
var userSettingsService = new Mock<IUserSettingsService>().Object;
_controller = new SettingsController(userSettingsService);
但即使在这种情况下...
... 限制模拟框架的使用
避免在你的测试类的每个测试中都包含与模拟框架相关的代码。这将使代码重构更加困难。尝试将所有此类代码集中在测试初始化和清理方法中。
有些时候这是不可能的。例如,几乎所有的测试可能都需要设置依赖项接口中某个方法的行为。这里你有几种选择
配置方法
在你的测试类中,你可以创建辅助方法来帮助配置期望值
private void UserIsNotAuthenticated(Mock<IAuthenticationService> authenticationService)
{
authenticationService.Setup(s => s.IsAuthenticated(It.IsAny<HttpRequestBase>())).Returns(false);
}
现在你可以在测试中使用这些方法,而不是直接使用模拟框架
UserIsNotAuthenticated(_authenticationService);
var result = (RedirectResult)_controller.Index(new LogonModel
{
ReturnUrl = FakeUrl
});
Assert.AreEqual(FakeUrl, result.Url);
这将减少模拟框架对你的测试的影响,并简化重构。
预配置的模拟对象
配置方法可以在一个测试类中使用时提供帮助。但是,如果你需要在多个测试类中定义某个方法的行为,那么还有另一种可能的方案。你可以创建预配置的模拟对象
internal class AuthenticationServiceStub : Mock<IAuthenticationService>
{
public AuthenticationServiceStub()
{
Setup(s => s.GetUserId(It.IsAny<string>())).Returns(1);
}
public void SetIsPasswordKeyValidTo(bool value)
{
Setup(s => s.IsPasswordKeyValid(It.IsAny<string>())).Returns(value);
}
// other code
}
在这种情况下,你手动创建模拟对象类,并从模拟框架的类派生它。在这里,你可以封装所有实例的通用配置(在构造函数中),并定义具有有意义名称的辅助方法来配置单个成员的行为。
使用这样的预配置模拟对象非常简单
[TestInitialize]
public void TestInitialize()
{
_authenticationService = new AuthenticationServiceStub();
_controller = new PasswordController(_authenticationService.Object);
}
以及它的配置方法
_authenticationService.SetIsPasswordKeyValidTo(false);
var result = (ViewResult)_controller.Index(Constants.Domain, "invalidKey");
Assert.AreEqual("Error", result.ViewName);
复杂的模拟对象行为
有些时候,你希望模拟对象具有复杂的行为。例如,你可能希望某个接口的多个方法协同工作并提供一致的信息。虽然可以使用模拟框架来实现它,但我建议你抵制这种诱惑。手动实现这样的模拟对象
public class PermissionsProviderStub : IPermissionsProvider
{
internal const string NoPermissions = "NoPermissions";
internal const string ReadPermission = "ReadPermission";
public Permissions GetPermissions(string objectId, int userId)
{
if (objectId.Equals(NoPermissions))
return new NoPermissions();
if (objectId.Equals(ReadPermission))
return new Permissions { HasReadAccess = true };
throw new ArgumentException($"Stub does not support ObjectId: {objectId} not supported");
}
}
这样更清晰,更容易理解。
尽管如此,即使在这种情况下,模拟框架也可以帮助你...
部分实现
有些接口具有非常长的成员列表。我知道,在许多情况下,我们应该将这样的接口分解成更小的接口。但它们仍然存在。你可能希望为其中的几个成员实现复杂的行为,而不在乎其他成员。在这种情况下,你可以使用接口的部分实现
public class LanguageProviderStub : Mock<ILanguageProvider>
{
public LanguageProviderStub()
{
Setup(p => p.GetPreferredLanguage(It.IsAny<int>(),
It.IsAny<List<int>>(), It.IsAny<int>()))
.Returns((Func<int, List<int>, int, Lang>)GetPreferredLanguage);
Setup(p => p.GetPreferredLanguageId(It.IsAny<int>(),
It.IsAny<List<int>>(), It.IsAny<int>()))
.Returns((Func<int, List<int>, int, int>)GetPreferredLanguageId);
}
private Lang GetPreferredLanguage
(int language, List<int> available, int defaultLanguage)
{
if (available.Contains(language))
return new Lang(language);
return new Lang(defaultLanguage);
}
private int GetPreferredLanguageId
(int language, List<int> available, int defaultLanguage)
{
var lang = GetPreferredLanguage(language, available, defaultLanguage);
return lang != null ? lang.LangId : -1;
}
}
你将你的实现分配给选定的方法,并让模拟框架处理剩下的部分
var controller = new LanguageController(new LanguageProviderStub().Object);
重构
我想提醒你,使用模拟框架并不意味着你不应该重构你的测试代码。你可能从为某个接口简单地使用模拟框架开始。但后来,随着你的测试的演化和增长,你可能会考虑编写配置方法甚至预配置的模拟对象。你现在使用模拟框架的方式并非一成不变,将来可能需要更改。
结论
让我总结一下本文的信息。
- 明智地选择你的模拟框架。
- 如果你对模拟成员的默认值感到满意,就使用它。
- 限制框架的使用:使用配置方法和预配置的模拟对象。
- 手动实现复杂的行为。在某些情况下,允许使用部分实现。
- 重构仍然是必须的。