【问题标题】:How to use NSubstitute and/or AutoFixture to test a concrete class如何使用 NSubstitute 和/或 AutoFixture 测试具体类
【发布时间】:2015-06-12 21:14:42
【问题描述】:

我希望通过使用 AutoFixture 和 NSubstitue,我可以充分利用它们各自提供的功能。我自己使用 NSubstitute 取得了一些成功,但我完全不知道如何将它与 AutoFixture 结合使用。

下面的代码显示了我正在尝试完成的一组事情,但我的主要目标是完成以下场景:测试方法的功能。

  1. 我希望使用随机值调用构造函数(也许一个除外 - 请阅读第 2 点。)。
  2. 无论是在构建期间还是之后,我都想更改属性的值 -Data
  3. 接下来拨打Execute并确认结果

我正在尝试进行的测试是:“should_run_GetCommand_with_provided_property_value”

任何帮助或参考说明如何使用 NSubstitue 和 AutFixture 的文章都会很棒。

示例代码:

using FluentAssertions;
using NSubstitute;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoNSubstitute;
using Xunit;

namespace RemotePlus.Test
{
    public class SimpleTest
    {
        [Fact]
        public void should_set_property_to_sepecified_value()
        {
            var sut = Substitute.For<ISimple>();
            sut.Data.Returns("1,2");

            sut.Data.Should().Be("1,2");
        }

        [Fact]
        public void should_run_GetCommand_with_provided_property_value()
        {
            /* TODO:  
             * How do I create a constructor with AutoFixture and/or NSubstitute such that:
             *   1.  With completely random values.
             *   2.  With one or more values specified.
             *   3.  Constructor that has FileInfo as one of the objects.
             * 
             * After creating the constructor:
             *   1.  Specify the value for what a property value should be - ex: sut.Data.Returns("1,2");
             *   2.  Call "Execute" and verify the result for "Command"
             * 
             */
            // Arrange
            var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
//            var sut = fixture.Build<Simple>().Create();  // Not sure if I need Build or Freeze            
            var sut = fixture.Freeze<ISimple>();  // Note: I am using a Interface here, but would like to test the Concrete class
            sut.Data.Returns("1,2");

            // Act
            sut.Execute();

            // Assert (combining multiple asserts just till I understand how to use NSubstitue and AutoFixture properly
//            sut.Received().Execute();
            sut.Data.Should().Be("1,2");
            sut.Command.Should().Be("1,2,abc");
            // Fails with : FluentAssertions.Execution.AssertionFailedExceptionExpected string to be "1,2,abc" with a length of 7, but "" has a length of 0.

        }
    }

    public class Simple : ISimple
    {

        // TODO: Would like to make this private and use the static call to get an instance
        public Simple(string inputFile, string data)
        {
            InputFile = inputFile;
            Data = data;

            // TODO: Would like to call execute here, but not sure how it will work with testing.
        }

        // TODO: Would like to make this private
        public void Execute()
        {
            GetCommand();
            // Other private methods
        }

        private void GetCommand()
        {
            Command = Data + ",abc";            
        }

        public string InputFile { get; private set; }
        public string Data { get; private set; }

        public string Command { get; private set; }


        // Using this, so that if I need I can easliy switch to a different concrete class
        public ISimple GetNewInstance(string inputFile, string data)
        {
            return new Simple(inputFile, data);
        }

    }

    public interface ISimple
    {
        string InputFile { get; }   // TODO: Would like to use FileInfo instead, but haven't figured out how to test.  Get an error of FileNot found through AutoFixture
        string Data { get; }
        string Command { get; }

        void Execute();
    }
}

【问题讨论】:

  • 在构造期间或之后定义或分配特定值:stackoverflow.com/q/28350054/126014
  • @MarkSeemann 感谢您的 cmets。您的博客、书籍、代码等对我帮助很大。然而,目前我的困惑更多是如何使用 NSubstitue 和 AutoFixture 的组合。您提供的解决方案仅针对 AutoFixture。再次感谢您分享您的知识和见解。
  • 你想解决什么问题?

标签: c# unit-testing autofixture nsubstitute


【解决方案1】:

我并没有真正使用过 AutoFixture,但根据一些阅读和一些尝试和错误,我认为您误解了它会为您做什么和不会做什么。在基本层面上,它可以让您创建对象图,根据对象构造函数(可能还有属性,但我没有研究过)为您填充值。

使用 NSubstitute 集成不会使您的类的所有成员成为 NSubstitute 实例。相反,它使夹具框架能够创建抽象/接口类型作为替代品。

查看您尝试创建的类,构造函数采用两个string 参数。这些都不是抽象类型,也不是接口,因此 AutoFixture 只会为您生成一些值并将它们传递进来。这是 AutoFixture 的默认行为,基于 cmets 中 @Mark Seemann 链接到的 answer 这是按设计。那里有他提出的各种变通方法,如果对你来说真的很重要,你可以实施,我不会在这里重复。

您已在 cmets 中表明您确实想将 FileInfo 传递给您的构造函数。这导致 AutoFixture 出现问题,因为它的构造函数接受一个字符串,因此 AutoFixture 向它提供了一个随机生成的字符串,这是一个不存在的文件,因此您会收到错误。尝试隔离以进行测试似乎是一件好事,因此 NSubstitute 可能有用。考虑到这一点,我建议您可能想要重写您的类并测试如下内容:

首先为FileInfo 类创建一个包装器(请注意,根据您所做的事情,您可能希望实际包装您想要的 FileInfo 中的方法,而不是将其作为属性公开,以便您实际上可以将自己与文件系统隔离开来,但暂时可以这样做):

public interface IFileWrapper {
    FileInfo File { get; set; }
}

在您的ISimple 界面中使用它而不是string(请注意,我已删除 Execute,因为您似乎不希望它在那里):

public interface ISimple {
    IFileWrapper InputFile { get; }   
    string Data { get; }
    string Command { get; }
}

Simple 来实现接口(我还没有解决你的私有构造函数问题,或者你在构造函数中调用 Execute):

public class Simple : ISimple {

    public Simple(IFileWrapper inputFile, string data) {
        InputFile = inputFile;
        Data = data;
    }

    public void Execute() {
        GetCommand();
        // Other private methods
    }

    private void GetCommand() {
        Command = Data + ",abc";
    }

    public IFileWrapper InputFile { get; private set; }
    public string Data { get; private set; }

    public string Command { get; private set; }
}

然后是测试:

public void should_run_GetCommand_with_provided_property_value() {
    // Arrange
    var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

    // create and inject an instances of the IFileWrapper class so that we 
    // can setup expectations
    var fileWrapperMock = fixture.Freeze<IFileWrapper>();

    // Setup expectations on the Substitute.  Note, this isn't needed for
    // this test, since the sut doesn't actually use inputFile, but I've
    // included it to show how it works...
    fileWrapperMock.File.Returns(new FileInfo(@"c:\pagefile.sys"));


    // Create the sut.  fileWrapperMock will be injected as the inputFile
    // since it is an interface, a random string will go into data
    var sut = fixture.Create<Simple>();

    // Act
    sut.Execute();


    // Assert - Check that sut.Command has been updated as expected
    Assert.AreEqual(sut.Data + ",abc", sut.Command);

    // You could also test the substitute is don't what you're expecting
    Assert.AreEqual("pagefile.sys", sut.InputFile.File.Name);
}

我没有使用上面的流利断言,但你应该能够翻译......

【讨论】:

  • 感谢您的详细回复。你是对的,我想多了,也许 AutoFixture 不适合这个例子。我设法通过对我的代码进行一些小的更改来测试我需要什么。
【解决方案2】:

实际上,我意识到我不需要在当前场景中使用 AutoFixture,从而找到了解决方案。

我不得不对我的代码进行一些更改:

  1. 添加了默认构造函数。
  2. 将我要为其提供默认值的方法和属性标记为“虚拟”。

理想情况下,我不想做这些事情,但这足以让我开始并让我现在继续前进。

帮助很大的链接:

修改代码:

using FluentAssertions;
using NSubstitute;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoNSubstitute;
using Xunit;
using Xunit.Abstractions;

namespace Try.xUnit.Tests
{
    public class TestingMethodCalls
    {
        private readonly ITestOutputHelper _output;

        public TestingMethodCalls(ITestOutputHelper output)
        {
            _output = output;
        }


        [Fact]
        public void should_set_property_to_sepecified_value()
        {
            var sut = Substitute.For<ISimple>();
            sut.Data.Returns("1,2");

            sut.Data.Should().Be("1,2");
        }

        [Fact (Skip="Don't quite understand how to use AutoFixture and NSubstitue together")]
        public void should_run_GetCommand_with_provided_property_value_old()
        {
            /* TODO:  
             * How do I create a constructor with AutoFixture and/or NSubstitute such that:
             *   1.  With completely random values.
             *   2.  With one or more values specified.
             *   3.  Constructor that has FileInfo as one of the objects.
             * 
             * After creating the constructor:
             *   1.  Specify the value for what a property value should be - ex: sut.Data.Returns("1,2");
             *   2.  Call "Execute" and verify the result for "Command"
             * 
             */
            // Arrange
            var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
//            var sut = fixture.Build<Simple>().Create();  // Not sure if I need Build or Freeze            
            var sut = fixture.Freeze<ISimple>();  // Note: I am using a Interface here, but would like to test the Concrete class
            sut.Data.Returns("1,2");

            // Act
            sut.Execute();

            // Assert (combining multiple asserts just till I understand how to use NSubstitue and AutoFixture properly
//            sut.Received().Execute();
            sut.Data.Should().Be("1,2");
            sut.Command.Should().Be("1,2,abc");
            // Fails with : FluentAssertions.Execution.AssertionFailedExceptionExpected string to be "1,2,abc" with a length of 7, but "" has a length of 0.
        }

        /* Explanation:
         * Create a construtor without any arguments.
         *      Had to create a parameterless constructor just for testing purposes (would like to improve on this)
         * Specify a default value for the desired method or property.
         *      It is necessary that the property or method has to be virtual.
         *      To specify that the based mehod should be call use the "DoNotCallBase" before the "Returns" call
         */ 
        [Fact]
        public void should_run_GetCommand_with_provided_Method_value()
        {
            // Arrange
            var sut = Substitute.ForPartsOf<Simple>();
            sut.When(x => x.GetData()).DoNotCallBase();
            sut.GetData().Returns("1,2");

            // Act
            sut.Execute();

            // Assert
            sut.Received().GetData();

            sut.Data.Should().Be("1,2");
            sut.Command.Should().Be("1,2,abc");
        }

        [Fact]
        public void should_run_GetCommand_with_provided_Property_value()
        {

            // Arrange
            var sut = Substitute.ForPartsOf<Simple>();
            sut.When(x => { var data = x.Data; }).DoNotCallBase();
            sut.Data.Returns("1,2");

            // Act
            sut.Execute();

            // Assert
            sut.Received().GetData();
            _output.WriteLine(sut.Command);

            sut.Data.Should().Be("1,2");
            sut.Command.Should().Be("1,2,abc");
        }

    }

    public class Simple : ISimple
    {
        public Simple(){}

        // TODO: Would like to make this private and use the static call to get an instance
        public Simple(string inputFile, string data)
        {
            InputFile = inputFile;
            InputData = data;

            // TODO: Would like to call execute here, but not sure how it will work with testing.
        }

        public virtual string GetData()
        {
            // Assume some manipulations are done
            return InputData;
        }

        // TODO: Would like to make this private
        public void Execute()
        {
            Data = GetData();
            GetCommand();
            // Other private methods
        }

        private void GetCommand()
        {
            Command = Data + ",abc";            
        }

        string InputData { get; set; }

        public string InputFile { get; private set; }


        public virtual string Data { get; private set; }
        public string Command { get; private set; }


        // Using this, so that if I need I can easliy switch to a different concrete class
        public ISimple GetNewInstance(string inputFile, string data)
        {
            return new Simple(inputFile, data);
        }

    }    

    public interface ISimple
    {
        string InputFile { get; }   // TODO: Would like to use FileInfo instead, but haven't figured out how to test.  Get an error of FileNot found through AutoFixture
        string Data { get; }
        string Command { get; }

        void Execute();
    }

}

【讨论】:

  • @我发布了另一个采用不同方法的答案。模拟您正在测试的课程通常是一个坏主意。您所有的 Data.Should 行实际上都在测试 NSubstitute 设置/行为,而不是底层类。通过使用 ForPartsOf 并设置 Data 返回值,您可以将类的实现与测试紧密耦合。对于这个例子来说它可能工作正常,但是当你开始编写更复杂的类时它可能会给你带来麻烦。
【解决方案3】:

我将其作为单独的答案发布,因为它更多的是对方法的批评,而不是直接回答您的原始问题。在我的other answer 中,我尝试直接回答您的 AutoFixture/NSubstitute 问题,假设您目前正在尝试将这些知识学习到框架中。

就目前而言,您实际上并不需要使用这些框架中的任何一个来实现您正在做的事情,并且在某些方面更容易不这样做。看看这个测试:

public void should_set_property_to_sepecified_value()
{
    var sut = Substitute.For<ISimple>();
    sut.Data.Returns("1,2");

    sut.Data.Should().Be("1,2");
}

这根本不是在测试你的类(除了编译检查),你实际上是在测试 NSubstitute。您正在检查您是否告诉 NSubstitute 返回它所执行的属性的值。

一般来说,尽量避免嘲笑您正在测试的课程。如果您需要这样做,那么您很有可能需要重新考虑您的设计。模拟对于向您的类提供依赖项非常有用,您可以控制这些依赖项以影响您的类的行为。如果您开始使用模拟修改您正在测试的类的行为,那么很容易对您实际测试的内容感到困惑(并创建非常脆弱的测试)。

因为您处理的是基本类型而不是嵌套对象,所以目前无需使用 AutoFixture/NSubstitute 之类的工具即可轻松创建和测试您的对象。您的代码可能看起来像这样,这似乎更接近您的期望:

public interface ISimple {
    string InputFile { get; }   
    string Data { get; }
    string Command { get; }
}

public class Simple : ISimple {
    private Simple(string inputFile, string data) {
        InputFile = inputFile;
        Data = data;
    }

    private void Execute() {
        GetCommand();
    }

    private void GetCommand() {
        Command = Data + ",abc";
    }

    public string InputFile { get; private set; }
    public string Data { get; private set; }

    public string Command { get; private set; }


    // Note.. GetNewInstance is static and it calls the Execute method
    static public ISimple GetNewInstance(string inputFile, string data) {
        var simple =  new Simple(inputFile, data);
        simple.Execute();
        return simple;
    }

}

您的测试将如下所示:

[Test]
public void should_run_GetCommand_with_provided_property_value() {
    // Arrange
    var inputFile = "someInputFile";
    var data = "1,2";
    var expectedCommand = "1,2,abc";


    // Act
    // Note, I'm calling the static method to create your instance
    var sut = Simple.GetNewInstance(inputFile, data);

    // Assert
    Assert.AreEqual(inputFile, sut.InputFile);
    Assert.AreEqual(data, sut.Data);
    Assert.AreEqual(expectedCommand, sut.Command);
}

我将Execute 留在了对象构造函数之外,因为它感觉有点像它要做的太多了。除了在构造函数中进行基本设置之外,我不太喜欢做很多事情,特别是如果你有可能最终得到calling virtual methods。我还将GetNewInstance 设为静态,以便可以直接调用它(否则您必须创建一个Simple 来调用GetNewInstance,这似乎是错误的)...

虽然我已经在上面展示了您的代码如何按照您的意愿工作,但我建议您可能希望将 Simple 构造函数更改为内部的,而不是私有的。这将允许您创建 factory 来创建实例。如果你有这样的事情:

public interface IMyObjectFactory {
    ISimple CreateSimple(string inputFile, string data);
}

public class MyObjectFactory {
    ISimple CreateSimple(string inputFile, string data) {
        var simple =  new Simple(inputFile, data);
        simple.Execute();
        return simple;
    }
}

这使您可以安全地构造需要对其调用方法的对象。您还可以在依赖于Simple 类的未来类中注入IMyObjectFactory 的替代品,它返回ISimple 的替代品。这有助于您将您的类与底层类行为(可能访问文件系统)隔离开来,并使您可以轻松地存根响应。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多