65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017年3月10日

CPOL

3分钟阅读

viewsIcon

6535

何时可以使用,以及何时不应该使用模拟框架

让我们来探讨何时可以使用,以及何时不应该使用模拟框架。同时,我也会展示一些在使用模拟框架时可能用到的模式。

如果你并不在意...

有时,你想要测试的类(被测系统,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);

重构

我想提醒你,使用模拟框架并不意味着你不应该重构你的测试代码。你可能从为某个接口简单地使用模拟框架开始。但后来,随着你的测试的演化和增长,你可能会考虑编写配置方法甚至预配置的模拟对象。你现在使用模拟框架的方式并非一成不变,将来可能需要更改。

结论

让我总结一下本文的信息。

  • 明智地选择你的模拟框架。
  • 如果你对模拟成员的默认值感到满意,就使用它。
  • 限制框架的使用:使用配置方法预配置的模拟对象
  • 手动实现复杂的行为。在某些情况下,允许使用部分实现
  • 重构仍然是必须的。
如何在测试中使用模拟框架 - CodeProject - 代码之家
© . All rights reserved.