【问题标题】:How do I unit test a method like this using IOC如何使用 IOC 对这样的方法进行单元测试
【发布时间】:2011-07-21 01:56:46
【问题描述】:

我正在尝试对一个看起来像这样的函数进行单元测试:

public List<int> Process(int input)
{
    List<int> outputList = new List<int>();

    List<int> list = this.dependency1.GetSomeList(input);
    foreach(int element in list)
    {
        // ... Do some procssing to element

        //Do some more processing
        int processedInt = this.dependency2.DoSomeProcessing(element);

        // ... Do some processing to processedInt

        outputList.Add(processedInt);
    }
    return outputList;
}

我计划在测试用例中模拟dependency1 和dependency2,但我不确定应该如何设置它们。为了设置dependency2.DoSomeProcessing,我需要知道每次调用“元素”的值。要弄清楚这一点,我要么需要:

  1. 将我的 Process() 方法中的一些逻辑复制粘贴到测试用例中

  2. 手动计算值(在实际函数中,这将涉及将一些带有许多小数位的双精度数硬编码到测试用例中)

  3. 咬紧牙关,使用dependency2的实际实现而不是模拟它。

这些解决方案对我来说都不是很好。我是国际奥委会的新手并且在嘲笑,所以我希望有一些我完全没有得到的东西。如果不是,这些解决方案中哪一个看起来最不糟糕?

谢谢

编辑:我正在使用 Moq 来模拟依赖项。

【问题讨论】:

    标签: c# unit-testing dependency-injection inversion-of-control


    【解决方案1】:

    你到底在测试什么?

    这个方法的内容是调用一个函数得到一个值列表,然后调用另一个函数修改这些值并返回修改后的值。

    您不应该在此测试中测试获取值的逻辑或转换它的逻辑,它们是单独的测试。

    所以你的嘲笑应该是(手工完成起订量)

    var dependency1 = new Mock<IDependency1>()
      .Setup(d => d.GetSomeList(It.IsAny<int>())
      .Returns(new List<int>(new [] { 1, 2, 3 });  
    var dependency2 = new Mock<IDependency2>()
      .Setup(d => d.DoSomeProcessing(It.IsAny<int>())
      .Returns(x => x * 2); // You can get the input value and use it to determine an output value
    

    然后只需运行您的方法并确保返回的列表是 2、4、6。这验证了对某个列表执行了某些处理。

    然后您创建一个不同的测试来确保GetSomeList() 有效。另一个确保DoSomeProcessing() 有效。 DoSomeProcessing() 测试需要手动计算值。

    【讨论】:

    • 啊,我不知道您可以根据最小起订量的输入来计算返回值。实际的函数签名看起来更像这样: bool Evaluate(input1, input2, out output) 那么我如何将 input1 和 input2 的输出作为基础输出呢?或者你能指点我解决这个问题的起订量文档的方向吗?谢谢!
    • @rob .Returns((int p1, int p2)=> p1 * p2);出局是另一回事。请参阅此处stackoverflow.com/questions/1881132/…,尽管您真的不需要担心输入,您只是想在输出中获取一个值,您可以对其进行测试并知道代码已被触发。
    • 感谢您的链接。太可惜了 Moq 不支持模拟参数。但如果我忽略输入,那么我将忽略“DoSomeProcessing()”之前的一些逻辑,我将无法测试它是否正确完成。
    • 我想我会这样做并更改我的函数,使其没有任何输出参数。谢谢!
    【解决方案2】:

    变量“元素”在 foreach 的开头和 DoSomeProcessing(element) 方法之间是否发生了变化?如果没有,我会选择 2 号选项。由于您可以控制依赖项 1 和依赖项 2,因此您可以为它们创建一个存根,以便返回列表中的一些元素并在依赖项 2 中编写代码来处理您获得的每个元素从dependency1 并返回正确的整数。

    我的第二个选项是 3。我不喜欢第一个。

    顺便说一下,看看 Pex (http://research.microsoft.com/en-us/projects/pex/) 在这种情况下是否对您有用,因为您已经完成了实际代码。

    []的

    【讨论】:

    • 是的“元素”在 foreach 循环开始和 DoSomeProcessing 之间发生了变化
    • Pex 看起来很有趣。我一定会调查的。
    【解决方案3】:

    好的,首先,如果您可以访问此方法并且可以更改它,那么更好的方法如下:

    public IEnumerable<int> Process(int input)
    {
        foreach(var element in dependency1.GetSomeList(input))
        {
            yield return dependency2.DoSomeProcessing(element);
        }
    }
    

    那么现在开始讨论手头的问题。方法中有 2 个依赖项需要被模拟才能正确测试。有一些很好的方法可以做到这一点,但最简单的是使用构造函数依赖注入。这是您通过构造函数将依赖项注入类的地方。您还需要您的依赖项具有通用的基类型(接口或抽象类)来执行注入。

    public class Processor
    {
        public Processor(IDep1 dependency1, IDep2 dependency2)
        {
            _dependency1 = dependency1;
            _dependency2 = dependency2;
        }
    
        IDep1 _dependency1;
        IDep2 _dependency2;
    
        public IEnumerable<int> Process(int input)
        {
            foreach(var element in dependency1.GetSomeList(input))
            {
                yield return dependency2.DoSomeProcessing(element);
            }
        }
    }
    

    现在创建一个模拟来注入它以进行测试。

    public interface IDep1
    {
        IEnumerable<int> GetSomeList(int);
    }
    
    public interface IDep2
    {
        int DoSomeProcessing(int);
    }
    
    public class MockDepdency1 : IDep1
    {
        public IEnumerable<int> GetSomeList(int val)
        {
            return new int[]{ 1, 2, 3 }.AsEnumerable();
        }
    }
    
    public class MockDepdency2 : IDep2
    {
        public int DoSomeProcessing(int val)
        {
            return val + 1;
        }
    }
    
    ...
    main()
    {
        IDep1 dep1 = new MockDependency1();
        IDep2 dep2 = new MockDependency2();
    
        var proc = new Processor(dep1, dep2);
        var actual = proc.Process(5);
    
        Assert.IsTrue(actual.Count() == 3);
    }
    

    我没有在编译器中编写此代码 - 我只是手动输入了它,但它应该接近工作并且代表了如何测试该方法的一个很好的示例。

    更好的方法是使用像 Unity 这样的实用程序,而不是通过构造函数进行注入。相反,您可以将您的接口映射到测试中的 MockDependency1 和 MockDependency2 ,当 Process(...) 运行时,它将获得 Mock 版本。如果你想知道怎么做,请告诉我,我会在下面添加。

    【讨论】:

    • 我认为这会奏效。我可能应该提到我正在嘲笑 Moq。我被挂断的事情是,我认为您必须在设置模拟方法时将文字输入指定给模拟方法,但事实并非如此。但我仍然没有想出从 Moq 中的输入生成输出和“输出”参数...
    • 使用依赖项的想法是可以将它们从我们要测试的代码中抽象出来。将它们抽象出来意味着它们可以从方程中移除,它们在方程中的输入会被仔细权衡,并测试方程的输出是否正确。一旦我们手动创建了一个模拟对象(如上),我们现在就在依赖项和相关方法之间创建紧密耦合。如果任何一个依赖项的功能发生变化,您的单元测试就会中断,因为我们的模拟对象不再与它试图模拟的真实对象同步。
    • +1 来自我...我不能不同意,但如果您不想学习模拟框架,这仍然是一个很好的方法。 Moq 是我的首选框架 - 我也喜欢 TypeMock。
    【解决方案4】:

    模拟两个依赖项

    最好的解决方案是模拟出你的两个依赖项。理想情况下,您的具体类应该在构造时注入这些依赖项。

    您只需要在 Process 中测试 3 件事。

    1. 根据模拟输入正确输出 Process
    2. 行为 =>(是否按预期调用了 dependency1.GetSomeList 等)
    3. 沿所有逻辑路径正确输出 Process

    你永远不应该使用具体的依赖,这会导致你有脆弱的单元测试,因为你将依赖于你无法控制的数据。要正确测试方法,您需要提供无效输入(空值、空字符串、极大数字、负数)等,以试图破坏当前预期的功能。以及自然有效的输入。

    您也不需要复制依赖项的实际功能。您唯一需要测试的是代码在所有逻辑路径上的正确功能。如果您需要更改dependency2 的输出以测试第二个代码路径。那是另一个测试。

    将您的测试分成大量细粒度的测试用例的好处是,如果您更改 Process 内部的任何内容,并运行您的单元测试。您确切地知道问题出在哪里,因为现在有 3 / 50 个测试失败了,并且您知道在哪里解决您的问题,因为它们都在测试 1 个特定的东西。

    使用 Rhino Mocks 会变成这样

    private IDependency1 _dependency1;
    private IDependency2 _dependency2;
    private ClassToBeTested _classToBeTested;
    
    [SetUp]
    private override void SetUp()
    {
      base.SetUp();
      _dependency1 = MockRepository.GenerateMock<IDependency1>();
      _dependency2 = MockRepository.GenerateMock<IDependency2>();
    
      _classToBeTested = new ClassToBeTested(_dependency1, _dependency2);
    
    }
    
    [Test]
    public void TestCorrectFunctionOfProcess()
    {
    
      int input = 10000;
      IList<int> returnList = new List<int>() {1,2,3,4};
    
      // Arrange   
      _dependency1.Expect(d1 => d1.GetSomeList(input)).Return(returnList);
      _dependency2.Expect(d2 => d2.DoSomeProcessing(0))
          .AtLeastOnce().IgnoreArguments().Return(1);
    
      // Act
      var outputList = _classToBeTested.Process(input);
    
      // Assert that output is correct for all mocked inputs
      Assert.IsNotNull(outputList, "Output list should not be null")
    
      // Assert correct behavior was _dependency1.GetSomeList(input) called?
      _dependency1.VerifyAllExpectations();
      _dependency2.VerifyAllExpectations();
    
    }
    

    更新

    IElementProcessor _elementProcessor;
    
    public List<int> Process(int input)
    {
        List<int> outputList = new List<int>();
    
        List<int> list = this.dependency1.GetSomeList(input);
        foreach(int element in list)
        {
            // ... Do some procssing to element
            _elementProcessor.ProcessElement(element);
    
            //Do some more processing
            int processedInt = this.dependency2.DoSomeProcessing(element);
    
            // ... Do some processing to processedInt
            _elementProcessor.ProcessInt(processedInt);
    
            outputList.Add(processedInt);
        }
        return outputList;
    }
    

    因此,上面发生的有效情况是,您的两个处理现在都被分解为单独的对象。过程几乎不是完全抽象的(这是完美的)。您现在可以单独测试每个元素,以确保在单独的单元测试中功能正确。

    上述测试将成为一个集成测试,您可以在其中测试每个依赖项是否被正确调用。

    【讨论】:

    • 谢谢。我以前没有使用过 Rhino Mock,但看起来您的示例代码假定 DoSomeProcessing 的输入始终为 0,但事实并非如此。这是我不确定如何计算的输入。虽然根据克里斯的回答,看起来我可能不需要计算它......
    • 在这种情况下,输入无关紧要,因此 Rhino 被告知 IgnoreArguments 并返回 1(0 仅作为占位符放置在那里,因为您不能为整数提供 null)。您唯一需要测试的不是 Process 如何执行其操作,而是它在其输出中为您提供正确的答案。恕我直言,您似乎应该将 ProcessElement 和 ProcessInt 抽象为两个单独的可测试方法/函数并分别对它们进行单元测试,以便您可以验证给定 X 的输入它会产生 Y。
    • 如果我忽略输入,那不会忽略一些我想测试的重要逻辑。我可以把它分成两种方法,但把它们都公开是没有意义的。所以测试内部受保护的会有点麻烦。
    • 将它们公开是有意义的,只是对这个特定对象不公开。我会用我的意思更新答案。
    • 啊我明白你的意思了。
    猜你喜欢
    • 2010-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-10
    • 2017-07-01
    • 2013-10-17
    相关资源
    最近更新 更多