c# - 在单元测试中使用伪造接口(interface)中未定义的方法

标签 c# unit-testing dependency-injection unity-container

在我的ViewModel中,根据登录用户的权限来启用/禁用部分功能。 ViewModel依赖于注入依赖项的ISecurity对象来检查用户是否具有特定权限。功能的不同部分需要不同的权限。

public Interface ISecurity
{
   bool UserHasPermision(int userId, string permission);
}


在我的生产代码中,ISecurity的具体实现与外部应用程序交互,该应用程序不允许我更改个人的权限。我创建了一个FakeSecurity类,该类允许我在单元测试中执行此操作。

class FakeSecurity: ISecurity
{
     private Dictionary<int, List<string>> permissions = new Dictionary<int, List<string>>();

     public bool UserHasPermission(int userId, string permission)
     {
         return permissions.ContainsKey(userId) && 
                permissions[userId].Contains(permission);
     }

     //Not defined in ISecurity
     public void SetPermission(int userId, string permission, bool hasPermission)
     {
         if (!permissions.ContainsKey(userId))
         {
              permissions[userId] = new List<string>();
         }
         List<string> userPermissions = permissions[userId];
         if (hasPermission) 
         {
              userPermissions.Add(permission);
         }
         else 
         {
              userPermissions.Remove(permission);
         }
     }
}


这里的问题是在SetPermission()接口中未定义ISecurity,因此,为了让我的单元测试设置个人权限,我需要将在IUnityContainer中注册的ISecurity对象转换为FakeSecurity对象。有人告诉我,单元测试应该不了解用于特定接口的特定类型的实现,并且未在接口中定义的调用方法是一种反模式。

[TestMethod]
public void UserDoesNotHavePermission()
{
   // test setup
   IUnityContainer iocContainer = GetIocContainer();
   ISecurity sec = iocContainer.Resolve<ISecurity>(); //registered singleton
   (sec as FakeSecurity).SetPermission(GetCurrentUser().Id, "Save Colors", false);
   var viewModel = iocContainer.Resolve<MaintainColorsViewModel>(); //per-request

   // asserts
   Assert.IsFalse(viewModel.CanSave);
}

[TestMethod]
public void UserHasPermission()
{
   // test setup
   IUnityContainer iocContainer = GetIocContainer();
   ISecurity sec = iocContainer.Resolve<ISecurity>(); //registered singleton
   (sec as FakeSecurity).SetPermission(GetCurrentUser().Id, "Save Colors", true);
   var viewModel = iocContainer.Resolve<MaintainColorsViewModel>(); //per-request

   // asserts
   Assert.IsTrue(viewModel.CanSave);
}


这是不好的做法吗?我意识到我不应该将ISecurity instace转换为应用程序代码中的特定类型,但这真的是单元测试的问题吗?

最佳答案

有人告诉我,单元测试应该不了解具体的实现类型


这是不正确的。让测试直接使用伪实现和被测类是完全正常和良好的做法。

但是,您在单元测试中使用的是DI容器,这实际上是不好的做法。尽管在编写集成测试时可以使用DI容器(因为您想测试与其他组件集成的组件),但是在单元测试中使用DI库会导致难以阅读和维护测试。使用单元测试,您可以独立测试代码。这意味着您通常手动创建要测试的类,并注入所需的伪依赖项以使测试运行。

因此,我希望这种单元测试看起来像这样:

public void CanSave_CurrentUserHasNoPermission_ReturnsFalse() {
    // Arrange
    var noPermission = new FakeSecurity { CurrentUserHasPermission = false };
    var viewModel = new MaintainColorsViewModel(noPermission);

    // Act
    bool actualResult = viewModel.CanSave;

    // Assert
    Assert.IsFalse(actualResult);
}

public void CanSave_CurrentUserHasPermission_ReturnsTrue() {
    // Arrange
    var hasPermission = new FakeSecurity { CurrentUserHasPermission = true };
    var viewModel = new MaintainColorsViewModel(hasPermission);

    // Act
    bool actualResult = viewModel.CanSave;

    // Assert
    Assert.IsTrue(actualResult);
}

public void CanSave_Always_QueriesTheSecurityForTheSaveColorsPermission() {
    // Arrange
    var security = new FakeSecurity();
    var viewModel = new MaintainColorsViewModel(security);

    // Act
    bool temp = viewModel.CanSave;

    // Assert
    Assert.IsTrue(security.RequestedPermissions.Contains("Save Colors"));
}    


关于此代码,需要注意以下几点:


FakeSecurityMaintainColorsViewModel都直接在此处的测试中创建;没有使用DI库。这使测试更具可读性和可维护性(并且更快)。
我大大简化了FakeSecurity类(如下所示),因为您希望假类尽可能简单。
添加了第三项测试以明确检查MaintainColorsViewModel是否请求期望的权限。
AAA模式(排列/动作/声明)被明确实现。


为了使这些测试按原样编写,对ISecurity抽象进行了以下更改:

interface ISecurity
{
    bool UserHasPermission(string permission);
}


userId参数已从UserHasPermission方法中删除。原因是ISecurity实现将能够找出当前用户本身是谁。允许ISecurity的使用者传递此参数仅意味着API变得越来越复杂,编写了更多代码,编程错误的可能性更大,因此我们需要更多的支持测试。换句话说,仅此userId属性的添加会迫使大量额外的生产和测试代码编写和维护。

这是简化的FakeSecurity类:

class FakeSecurity : ISecurity
{
    public bool CurrentUserHasPermission;
    public List<string> RequestedPermissions = new List<string>();

    public bool UserHasPermission(string permission)
    {
        this.RequestedPermissions.Add(permission);
        return this.CurrentUserHasPermission;
    }
}


现在,FakeSecurity类只有很少的代码,仅通过查看它就可以非常容易地检查其正确性。请记住,测试代码应尽可能简单。旁注:用生成的模拟对象替换此类不会使我们的代码更容易。在大多数情况下,这实际上会使我们的单元测试更加难以阅读,理解和维护。

开发人员开始在单元测试中使用DI容器的原因之一是因为手动创建被测类(及其所有虚假依赖项)会导致测试中的维护问题。实际上,这是真的。如果MaintainColorsViewModel具有多个依赖项,并且我们将在每个测试中创建该MaintainColorsViewModel,则添加单个依赖项将导致我们更改所有MaintainColorsViewModel测试。这通常是开发人员使用DI容器或还原为模拟框架的原因。

但是,这不是开始使用DI容器或模拟库的好理由。简单的重构可以完全消除维护问题;我们只需要创建一个工厂方法,如下所示:

private static MaintainColorsViewModel CreateViewModel(params object[] dependencies) {
    return new MaintainColorsViewModel(
        dependencies.OfType<ISecurity>().SingleOrDefault() ?? new FakeSecurity(),
        dependencies.OfType<ILogger>().SingleOrDefault() ?? new FakeLogger(),
        dependencies.OfType<ITimeProvider>().SingleOrDefault() ?? new FakeTimeProvider(),
        dependencies.OfType<IUserContext>().SingleOrDefault() ?? new FakeUserContext());
}


在这里,我假设MaintainColorsViewModel包含4个依赖项(即ISecurityILoggerITimeProviderIUserContext)。 CreateViewModel工厂方法允许使用params数组传入所有依赖关系,并且该方法尝试从数组中获取每个抽象,当缺少时将其替换为默认的伪实现。

有了这个工厂,我们现在可以将测试重写为以下内容:

[TestMethod]
public void CanSave_CurrentUserHasNoPermission_ReturnsFalse()
{
    // Arrange
    var noPermission = new FakeSecurity { CurrentUserHasPermission = false };

    MaintainColorsViewModel viewModel = CreateViewModel(noPermission);

    // Act
    bool actualResult = viewModel.CanSave;

    // Assert
    Assert.IsFalse(actualResult);
}


或者如果测试需要这样做,我们可以传递多个依赖项:

[TestMethod]
public void CanSave_CurrentUserHasNoPermission_LogsWarning()
{
    // Arrange
    var logger = new FakeLogger();
    var noPermission = new FakeSecurity { CurrentUserHasPermission = false };

    MaintainColorsViewModel viewModel = CreateViewModel(logger, noPermission);

    // Act
    bool temp = viewModel.CanSave;

    // Assert
    Assert.IsTrue(logger.Entries.Any());
}


请注意,此测试仅出于教育目的。我不建议视图模型实际进行日志记录。那不应该是它的责任。

这个故事的寓意实际上是,好的设计可以大大简化您的测试工作,以至于您可以编写更少的代码和更少的测试,同时提高软件的质量。

关于c# - 在单元测试中使用伪造接口(interface)中未定义的方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33812284/

相关文章:

C# 有什么方法可以通过代码访问静态类的类名?

java - Resources$NotFoundException 调用 Robolectric.buildActivity() 时

iOS - 运行 Swift 单元测试时找不到 'MyProject-Swift.h' 文件

android - 如何使用 Dagger 2.11 注入(inject)模拟

asp.net-mvc - 通过依赖注入(inject)将数据上下文对象传递给 Controller ​​是一个好主意吗?

c# - 如何使用方法 post 使用 HttpRequest 发送变量?

c# - C#中的另一个项目引用时如何使用已编译项目复制非代码文件

c# - 提取 Twitter PIN 信息,WP7

javascript - jest.doMock 和 JSON 导入模拟

c# - 在泛型类中注入(inject)依赖项时出错