【问题标题】:C# unit testing using mocks without interfaces使用没有接口的模拟进行 C# 单元测试
【发布时间】:2021-06-11 00:22:56
【问题描述】:

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

接口是其他开发人员对该类的期望的约定。因此,所有接口方法、属性等都是公共的。任何好的代码也应该有好的封装。因此,我不想将我的类中的所有方法都公开,因此不要在接口中声明它。因此,我无法模拟和设置这些在我的主类中使用的内部方法。

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

我搜索了有关 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()
        ...
    }
}

【问题讨论】:

  • 提供minimal reproducible example 的代码可能有助于显示您的问题(例如,您当前拥有的类结构以及您要进行单元测试的代码)
  • 这不是一个需要代码示例的问题。如果您不知道如何模拟接口或抽象类,那么您将无法解决问题。也许你不喜欢阅读,所以我总结一下。我需要模拟一个内部方法,同时将对象公开为一个接口,而不是仅仅为了测试目的而添加代码。
  • 你总结的方式与我在你原来的问题中读到的不同,但是模拟内部方法似乎违背了模拟框架的目的。
  • 我不确定我是否完全理解您所描述的问题,但是当我想表明接口主要是作为开发细节或作为注入点时,我'已将其声明为“内部接口 {}”。然后只有该程序集可以访问它(除非您还使用 InternalsVisibleTo 属性)。 “合同”仍然存在,但不能在依赖项目中使用。
  • 我尝试添加代码示例以使问题更清晰。代码比英文更容易理解:)

标签: c# .net unit-testing interface moq


【解决方案1】:

接口测试没有意义。接口没有说“它应该做什么”。当我需要使用接口测试某些东西时,我会在我的 NUnit 测试类中创建 MockClass。这个类只适用于少数测试,它是内部的。如果您的测试类和测试具有相同的命名空间,则应该有足够的内部空间。所以不公开。但是你仍然不能测试任何私有方法。

有时这很烦人,但我的测试中没有好的代码。但这并不奇怪。

【讨论】:

  • 可以使用“PrivateObject Class”测试私有方法。然而,对此的需要通常表明错误的测试。只应测试公共方法,但有时需要模拟公共方法内部使用的内部方法。
【解决方案2】:

我明白只测试公共方法和属性的意义,但有时这种限制毫无意义,就像使用接口来支持单元测试一样。

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

【讨论】:

  • 你是正确的,只有公共方法应该被测试。但是,这些公共方法可以从工厂创建的对象调用内部方法。在这些情况下,这个工厂类通常会被模拟并注入以提供模拟“帮助”类。这些帮助类可以有内部方法,如果在 mock 中不可用,这些方法会被调用并且会失败。
【解决方案3】:

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

internal interface IInitialize
{
    void Initialize();
}

然后在您的公共接口旁边的类中实现它:

public class MyService : IMyService, IInitialize

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

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&lt;TInterface&gt;() 方法来处理多个接口(阅读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&lt;TInterface&gt;()上的以下注释:

该方法只能在第一次使用mock之前调用 Moq.Mock`1.Object 属性,此时运行时类型具有 已经生成,不能再添加接口了。

还要注意As&lt;TInterface&gt;()的使用还需要以下属性才能允许mock代理访问实现内部接口:

[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]

【讨论】:

  • 感谢您抽出宝贵时间尝试寻找解决方案 :)。我起草了一个很长的回复,解释了为什么这仍然不能解决问题,但我受到评论中字符的限制,所以会给出主要问题。我基本上已经尝试过类似的方法,但是任何接口(即使它是“内部的”)都要求实现它的所有方法都是“公共的”。强迫我在不应该的时候在我的主类中将方法“公开”。
【解决方案4】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-10-13
    • 1970-01-01
    • 2012-02-16
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    相关资源
    最近更新 更多