由方法而不是构造函数创建的假对象

Fake object that is created by a method, not a constructor

我正在尝试对使用 API 的代码进行单元测试,所以我正在尝试解耦。

我在API里面为"Application"class创建了一个接口,是密封的

然后我创建了一个 class,它使用的接口有一个方法,return 是一个 "Application" 类型的对象。

这是我遇到问题的地方,在我的单元测试中,我尝试创建一个 "Application" 对象来验证 return 值是否正确。然而 "Application" class 没有任何构造函数,没有任何 public 或私有的(我用反射检查过)。该对象是通过调用静态 Application.Connect(AnotherTypeFromAPI arg) 创建的,它 return 是一个应用程序对象。

如何 return 我无法创建的假对象?

appMock.Connect(arg).Returns("How do I return an Application object here?"));

或者对于依赖于 API 的单元测试代码,我是否采用了错误的方式?整个 API 依赖于 "Application" 类型,所以如果我不能伪造它,我还不确定如何存根或模拟我需要的其他方法。

我正在使用 C#、NUnit、NSUbstitute。

您通常不会模拟或伪造静态方法,例如 Application.Connect。只需对被测代码进行分区,使其采用已创建的 IApplication 对象。

这个问题可以解决,但是你用错了模式。您需要创建一个 完全替换 具体依赖项的接口,而不是通过新接口公开应用程序的实例。

你有什么

如果我对你的问题的理解正确,你有一个密封的应用程序 class,它有一些你的程序需要能够调用的方法,它没有 public 构造函数,只有一个静态工厂方法。这里举一个简单的例子来讨论,只有一种方法,SomeMethod().

public sealed class Application
{
    //private ctor prevents anyone from using new to create this
    private Application()   
    {
    }

    //Here's the method we want to mock
    public void SomeMethod(string input)
    {
        //Implementation that needs to be stubbed or mocked away for testing purposes
    }

    //Static factory method
    static public Application GetInstance()
    {
        return new Application();
    }
}

你尝试了什么

您所做的可能如下所示:

interface IApplication
{
    Application Application { get; }
}

class ApplicationWrapper : IApplication
{
    protected readonly Application _application;

    public ApplicationWrapper()
    {
        _application = Application.GetInstance();
    }

    public Application Application
    {
        get { return _application; }
    }
}

所以在你的主要代码中,你这样做:

var a = new ApplicationWrapper();
a.Application.SomeMethod("Real argument");

这种方法永远不适用于单元测试,因为您仍然直接依赖于密封的应用程序 class。你刚刚移动了它。你仍然需要调用Application.SomeMethod(),这是一个具体的方法;你应该只依赖于界面,而不是任何具体的东西。

什么会起作用

理论上,"right" 的方法是包装所有东西。因此,不要将 Application 公开为 属性,而是将其保密;相反,您公开方法的包装版本,如下所示:

public interface IApplication
{
    void SomeMethod(string input);
}

public class ApplicationWrapper : IApplication
{
    protected readonly Application _application;

    public ApplicationWrapper()
    {
        _application = Application.GetInstance();
    }

    public void SomeMethod(string input)
    {
        _application.SomeMethod(input);
    }
}

然后你会这样称呼它:

var a = new ApplicationWrapper();
a.SomeMethod("Real argument");

或者完整的 class 与 DI,它看起来像这样:

class ClassUnderTest
{
    protected readonly IApplication _application; //Injected

    public ClassUnderTest(IApplication application)
    {
        _application = application; //constructor injection
    }

    public void MethodUnderTest()
    {
        _application.SomeMethod("Real argument");
    }
}

如何进行单元测试

在您的单元测试中,您现在可以使用新的 class 模拟 IApplication,例如

class ApplicationStub : IApplication
{
    public string TestResult { get; set; }  //Doesn't exist in system under test

    public void SomeMethod(string input)
    {
        this.TestResult = input;
    }
}

请注意,此 class 完全不依赖于应用程序。因此,您根本不再需要对其调用 new 或调用其工厂方法。对于单元测试目的,您只需要确保它被正确调用。您可以通过传入存根并随后检查 TestResult 来执行此操作:

//Arrange
var stub = new ApplicationStub();
var c = ClassUnderTest(stub);

//Act
c.MethodUnderTest("Test Argument");

//Assert
Assert.AreEqual(stub.TestResult, "Test Argument");

编写完整的包装器需要做更多的工作(特别是如果它有很多方法),但是您可以使用反射或第三方工具生成大量代码。它允许您进行完整的单元测试,这就是切换到 IApplication 界面的全部想法。

TLDR:

而不是

IApplication wrapper = new ApplicationWrapper();
wrapper.Application.SomeMethod();

你应该使用

IApplication wrapper = new ApplicationWrapper();
wrapper.SomeMethod();

删除对具体类型的依赖。