【问题标题】:Fake object that is created by a method, not a constructor由方法而不是构造函数创建的假对象
【发布时间】:2018-10-31 15:06:32
【问题描述】:

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

我在 API 中为“Application”类创建了一个接口,该接口是密封的。

然后我创建了一个使用接口的类,该接口具有一个返回“应用程序”类型对象的方法。

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

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

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

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

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

【问题讨论】:

    标签: c# mocking nunit nsubstitute


    【解决方案1】:

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

    你有什么

    如果我正确理解您的问题,您有一个密封的 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();
    

    去除对具体类型的依赖。

    【讨论】:

    • 感谢您的示例,我从这条路径开始,但该方法将 API 中的另一个对象作为参数,该参数也使用工厂方法创建对象实例。它已经到了我不得不包装大部分 API 的地步。我也意识到我开始测试 API 本身,而不是我的代码。
    【解决方案2】:

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

    【讨论】:

    • 我将此标记为正确答案,因为最初我正在测试 API,这不是我的代码。我所做的是创建自己的使用 API 的类。这样我就可以对我创建的类中的方法进行存根,以验证对第三方库的调用是否发生。分区允许我将 API 存根,以便我可以测试我的代码。
    猜你喜欢
    • 2013-03-13
    • 2013-09-24
    • 2014-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多