如何使用NSubstitute和/或AutoFixture来测试具体类

时间:2015-06-12 21:14:42

标签: c# unit-testing autofixture nsubstitute

我希望通过使用AutoFixture和NSubstitue,我可以使用每个人必须提供的最好的东西。我自己使用NSubstitute取得了一些成功,但我对如何将它与AutoFixture结合使用感到困惑。

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

  1. 我希望用随机值调用构造函数(除了一个 - 请阅读第2点)。
  2. 在施工期间或之后,我想更改某个属性的值 - Data
  3. 接下来致电Execute并确认结果
  4. 我正在尝试的测试是:“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();
        }
    }
    

3 个答案:

答案 0 :(得分:6)

我还没有真正使用过AutoFixture,但基于一些阅读和一些反复试验,我认为你错误地解释了它将会做什么,不会为你做什么。在基本层面,它可以让你创建一个对象图,根据对象构造函数为你填充值(可能还有属性,但我没有考虑过)。

使用NSubstitute集成不会将所有类成员放入NSubstitute实例中。相反,它为fixture框架提供了创建抽象/接口类型作为替换的能力。

查看您尝试创建的类,构造函数需要两个string个参数。这些都不是抽象类型或接口,因此AutoFixture将为您生成一些值并将其传入。这是AutoFixture的默认行为,并基于@Mark Seemann在评论中链接的answer这是设计的。他在那里提出了各种各样的工作,你可以实施,如果这对你来说真的很重要,我在此不再重复。

您已在评论中指出,您确实希望将FileInfo传递给构造函数。这导致AutoFixture成为问题,因为它的构造函数接受一个字符串,因此AutoFixture正在向它提供一个随机生成的字符串,这是一个不存在的文件,因此您会收到错误。尝试隔离测试似乎是件好事,因此NSubstitute可能对此有用。考虑到这一点,我建议你可能想要重写你的类并测试这样的东西:

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

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

ISimple界面而不是string中使用此功能(请注意我已删除执行,因为您似乎不想在那里执行):

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);
}

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

答案 1 :(得分:1)

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

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

  1. 添加了默认构造函数。
  2. 标记了我想为&#34;虚拟&#34;提供默认值的方法和属性。
  3. 理想情况下,我不想做这些事情,但这足以让我开始并让我继续前进。

    有很多帮助的链接:

    修改后的代码:

    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();
        }
    
    }
    

答案 2 :(得分:1)

我将此作为单独的答案发布,因为它更像是对方法的批评,而不是对原始问题的直接回答。在我的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;
    }
}

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