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

本文关键字:方法 单元测试 何使用 IOC | 更新日期: 2023-09-27 18:03:12

我正试图为一个函数编写单元测试,看起来像这样:

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。我需要知道每次调用"element"时它的值是多少。要弄清楚这一点,我要么需要:

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

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

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

这些解决方案对我来说都不是很好。我是IOC和嘲讽的新手,所以我希望有什么是我完全不明白的。如果不是,这些解决方案中哪一个看起来最不坏?

感谢

编辑:我使用Moq来模拟依赖关系。

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

你到底在测试什么?

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

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

所以你的嘲弄应该是这样的(hand compliwith 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()测试需要手动计算值。

变量"element"是否在foreach和DoSomeProcessing(element)方法的开始之间改变?如果不是,我就选第二种。由于您可以控制dependency1和dependency2,因此您可以为它们创建一个存根,以便您返回列表中的一些元素,并在dependency2中编写代码来处理从dependency1获得的每个元素并返回正确的整数。

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

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

[]'s

首先,如果你可以访问这个方法并且可以更改它那么一个更好的方法是:

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

现在创建一个mock,将其注入到测试中。

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

模拟这两个依赖

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

你只需要在Process中测试3个东西。

  1. 根据模拟输入正确输出工艺
  2. Behavior => (was dependency1.)getsomeelist按预期调用,等等)
  3. 进程沿着所有逻辑路径的正确输出

你不应该使用具体的依赖关系,这将导致你有脆弱的单元测试,因为你将依赖于你控制之外的数据。为了正确地测试一个方法,你需要提供无效的输入(null,空字符串,极大的数字,负数)等,任何试图打破当前预期功能的东西。以及自然有效的输入。

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

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

使用Rhino mock会像这样

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

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

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