使用没有接口的模拟的 C# 单元测试

C# unit testing using mocks without interfaces

为了正确地对我的一些 classes 进行单元测试,我需要模拟被测试的主要 class 使用的 class 对象。这是一个简单的过程,如果所有正在使用的对象都实现了接口 and 我只需要使用该接口的方法。然而,问题来了:

接口是关于其他开发人员在 class 中应该期待什么的契约。因此所有接口方法、属性等都是public。任何好的代码也应该有好的封装。因此,我不想在 class public 中创建所有方法,因此不在接口中声明它。结果我无法模拟和设置我的主要 class.

中使用的这些内部方法

我研究的其他选项是使用抽象 class,它可以有不同的访问修饰符,我可以在其中使用正确的方法,内部或私有等。但是因为我想要 class被模拟为可用的是供其他开发人员使用的 public 属性 接口类型,我需要它是一个接口,但现在它不再兼容,因为我的主要 class 无法调用内部方法不再定义为接口。第 22 条军规。(如果您想知道,对方法使用 virtual 会遇到与使用 abstract class 相同的问题)。

我搜索过有关 C# 模拟的类似问题,但没有找到与我相同的问题。并且请不要对“C# 不是用于测试的好语言”之类的东西抱有哲理。这不是答案。

有什么想法吗?

我添加了这个代码示例,以便更容易看到上述问题。

[assembly: InternalsVisibleTo("MyService.Tests")]

public interface IMyService
{
    MethodABC()
    ...
}

public class MyService : IMyService 
{
    public void MethodABC()
    {
        ...
    }
    
    internal void Initialize()
    {
        ...
    }
    ...
}

public sealed partial class MyMain
{
    public IMyService Service { get; private set; }
    private MyService _service;
    
    ...
    
    private void SomeMethod()
    {
        // This method is in interface and can be used outside the project when this assembly is referenced
        _service.MethodABC() 
        ...
        
        // This method is and internal method inside the class not to be seen or used outside the project. 
        // 1) When running this method through unit test that mocked the IMyService will fail here since initialize don't exist which is correct.
        // 2) Mocking the MyService will require "virtual" to be added to all methods used and don't provide a interface/template for a developers if 
        //    they want to swap the service out.
        // 3) Changing the interface to abstract class and mocking that allows for making this an internal method and also set it up in mock to do 
        //    something for unit test and provides contract other developers can implement and pass in to use a different service, but requires 
        //    "override" for all methods to be used from "outside" this assembly. This is best solution but require changes to code for sole purpose
        //    of testing which is an anti-pattern.
        _service.Initialize() 
        ...
    }
}

// Unit test method in test project

[TestClass]
public class MyMainTests
{

    private Mock<IMyService> _myServiceMock = new Mock<IMyService>();

    [TestMethod]
    public void MyMain_Test_SomeMethod()
    {
        ...
        SomeMethod()
        ...
    }
}

不幸的是,似乎没有一种干净的方法可以做到这一点。为了使用 Moq 创建模拟,它需要是接口、抽象 class 或虚拟方法。

  • 接口的封装不能低于 public。使用内部接口仍然会强制您创建“Public”方法。
  • 虚拟方法允许使用访问修饰符,但不提供可注入对象和合同,以供其他使用主要 class 的开发人员使用的合同 class。
  • 理想的解决方案不需要仅仅为了使其可单元测试而更改代码。不幸的是,这似乎是不可能的。

这让我想到了一个抽象 class,它可以提供一个可以像接口一样处理的模板(半接口),但需要对所有合同方法进行“覆盖”,但至少将允许正确的访问修饰符对于方法。

这仍然不利于干净的代码,因为我需要将代码添加到我的所有方法中,其唯一目的是使其可进行单元测试。

这是 Microsoft 可以研究的新 .Net C# 功能。我会检查他们是否已经有此功能请求。

接口测试没有意义。界面没有说明“它应该做什么”。当我需要用接口测试一些东西时,我在我的 NUnit 测试中制作 MockClass class。此 class 仅适用于少数测试,并且是内部的。如果你的测试 class 和你的测试有相同的命名空间,那么应该有足够的内部空间。所以不是public。但是你仍然不能测试任何私有方法。

有时这很烦人,但我无法在测试中编写漂亮的代码。但这并不奇怪。

我明白了只测试 public 方法和属性的意义,但有时这种限制毫无意义,而且使用接口只是为了支持单元测试。

我的解决方法是继承我正在测试的 class,添加 public 访问方法,然后从中调用受保护的基 class 成员。

要考虑的一个选项是在 internal interface 上指定内部操作,然后您可以使用它来模拟这些操作。根据您的示例,您可以添加:

internal interface IInitialize
{
    void Initialize();
}

然后在 class 中与 public 界面一起实现:

public class MyService : IMyService, IInitialize

然后您的消费 class 可以根据需要使用接口:

public sealed partial class MyMain
{
    public MyMain(IMyService myService)
    {
        Service = myService;
    }

    public IMyService Service { get; }

    public void SomeMethod()
    {
        (Service as IInitialize)?.Initialize();
        Service.MethodABC();
    }
}

现在在单元测试中,您可以利用 Moq 中的 As<TInterface>() 方法来处理多个接口(阅读 the docs):

[Fact]
public void Test1()
{
    Mock<IMyService> myServiceMock = new Mock<IMyService>();
    Mock<IInitialize> myServiceInitializeMock = myServiceMock.As<IInitialize>();
    //myServiceMock.Setup(s => s.MethodABC()).Whatever
    //myServiceInitializeMock.Setup(s => s.Initialize()).Whatever

    MyMain myMain = new MyMain(myServiceMock.Object);
    myMain.SomeMethod();

    myServiceMock.Verify(s => s.MethodABC(), Times.Once);
    myServiceInitializeMock.Verify(s => s.Initialize(), Times.Once);
}

请注意 As<TInterface>() 上的以下备注:

This method can only be called before the first use of the mock Moq.Mock`1.Object property, at which point the runtime type has already been generated and no more interfaces can be added to it.

另请注意,使用As<TInterface>()还需要以下属性,以允许模拟代理访问实现内部接口:

[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]