如何使用IOC对这样的方法进行单元测试

时间:2011-07-21 01:56:47

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

我正试图为一个看起来像这样的函数试验单元测试:

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,我需要知道每次调用“element”的值。为了解决这个问题,我要么:

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

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

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

  4. 这些解决方案对我来说都不是很好。我是IOC的新手并且嘲笑所以我希望有一些我完全没有得到的东西。如果不是这些解决方案中哪一个看起来最不好?

    由于

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

4 个答案:

答案 0 :(得分:1)

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

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

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

[]的

答案 1 :(得分:1)

你究竟在测试什么?

此方法的内容是它调用一个函数来获取值列表,然后调用另一个函数来修改这些值并返回修改后的值。

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

所以你的嘲讽应该是(手工编写Moq)

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()测试,需要手工计算的值。

答案 2 :(得分:1)

模拟两个依赖项

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

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

  1. 根据模拟输入正确输出过程
  2. 行为=&gt; (依赖于1.GetSomeList按预期调用等)
  3. 沿着所有逻辑路径确定Process的输出
  4. 您永远不应该使用具体的依赖项,这将导致您进行脆弱的单元测试,因为您将依赖于您无法控制的数据。要正确测试方法,您需要提供无效输入(空值,空字符串,极大数字,负数)等,以试图打破当前预期的功能。并且自然也是有效的输入。

    您也不需要复制依赖项的实际功能。您需要测试的唯一事项是沿着所有逻辑路径的代码的正确功能。如果需要更改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;
    }
    

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

    然后,上面的测试将成为集成测试,您可以测试每个依赖项是否正确调用。

答案 3 :(得分:1)

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

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

现在谈谈手头的问题。您需要模拟方法中的2个依赖项才能正确测试它。有一些很好的方法可以做到这一点,但最简单的方法是使用Constructor Dependency Injection。这是通过构造函数将依赖项注入到类中的位置。您将需要您的依赖项具有公共基类型(接口或抽象类)来执行注入。

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版本。如果你想知道如何做到这一点,请告诉我,我将在下面添加。