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

时间:2018-05-21 23:05:08

标签: c# mocking nunit nsubstitute

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

我为"应用程序"创建了一个界面。 API中的类,它是密封的。

然后我创建了一个使用该接口的类,该接口有一个返回" Application"类型对象。

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

如何返回无法创建的虚假对象?

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

或者我是否在依赖API的单元测试代码方面做错了?整个API依赖于"应用程序"如果我不能伪造它,我不知道我怎么能存根或嘲笑我需要的其他方法。

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

2 个答案:

答案 0 :(得分:2)

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

你有什么

如果我正确理解你的问题,你有一个密封的Application类,它有一些你的程序需要能够调用的方法,它没有公共构造函数,只有一个静态工厂方法。这是一个简单的讨论示例,只有一种方法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");

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

什么会起作用

理论上,“正确”的方法是包装所有内容。因此,不要将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");

或者在DI的完整课程中,它看起来像这样:

class ClassUnderTest
{
    protected readonly IApplication _application; //Injected

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

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

如何进行单元测试

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

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

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

请注意,此类完全不依赖于Application。所以你不再需要在它上面调用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();

删除对具体类型的依赖。

答案 1 :(得分:1)

您通常不会模仿或伪造静态方法,例如Application.Connect。只需对正在测试的代码进行分区,以便它采用已创建的IApplication对象。