我们如何正确模拟IEnumerable <T>?

时间:2020-03-05 18:38:16  来源:igfitidea点击:

我最近通过模拟遇到的灾难并没有使我感到困惑,因为我需要将结果实际推入IEnumerable &lt;T>模拟对象中。

这是一个示例(仅演示IEnumerable &lt;T>,实际上不是良好的基于​​交互的测试!):

using System;
using System.Collections.Generic;
using Rhino.Mocks;
using MbUnit.Framework;

[TestFixture]
public class ZooTest
{
    [Test]
    public void ZooCagesAnimals()
    {
        MockRepository mockery = new MockRepository();

        IZoo zoo = new Zoo();

        // This is the part that feels wrong to create.
        IList<IAnimal> mockResults = mockery.DynamicMock<IList<IAnimal>>();
        IAnimal mockLion = mockery.DynamicMock<IAnimal>();
        IAnimal mockRhino = mockery.DynamicMock<IAnimal>();

        using (mockery.Record())
        {
            Expect.Call(zoo.Animals)
                .Return(mockResults)
                .Repeat.Once();
        }

        using (mockery.Playback())
        {
            zoo.CageThe(mockLion);
            zoo.CageThe(mockRhino);

            Assert.AreEqual(mockResults, new List<IAnimal>(zoo.Animals));
        }       
    }
}

public class Zoo : IZoo
{
    private IList<IAnimal> animals = new List<IAnimal>();

    public void CageThe(IAnimal animal)
    {
        animals.Add(animal);
    }

    public IEnumerable<IAnimal> Animals
    {
        get
        {
            foreach(IAnimal animal in animals)
            {
                yield return animal;
            }
        }
    }
}

public interface IAnimal
{
}

public interface IZoo
{
    IEnumerable<IAnimal> Animals { get;}
    void CageThe(IAnimal animal);
}

我不喜欢如何使其工作,原因如下:

  • IEnumerable &lt;IAnimal>的结果消耗到IList &lt;IAnimal>中-因为我知道这会将结果检查到堆中。
  • 设置结果的内容-我也很了解;但是我的主要观点是要测试Zoo.Animals是否返回IEnumerable &lt;IAnimal>,甚至更好的是,它在内部使用了yield return

有什么建议可以做得更好或者更简单吗?

编辑:我正在尝试确定最佳的方法来测试IEnumerable <T>与我所使用的东西之间的交互。我不是要测试Zoo可以容纳动物,而是要Zoo暴露为IEnumerable &lt;IAnimal>,并且也要使用yield return

解决方案

回答

如果要测试实现,为什么首先要模拟它?为什么不只是CageThe(IAnimal)然后检查Animals是否包含该IAnimal?

我知道我们在嘲笑IAnimals,看来我们还没有任何具体的动物可以玩,但是为什么不直接制作它们,因为显然我们不希望它们发生任何其他变化放进清单?

编辑:大致遵循以下原则(未经测试,可能无法编译,可能会吞食狗等):

[TestFixture]
public class ZooTest 
{
    [Test]
    public void ZooCagesAnimals()
    {
        MockRepository mockery = new MockRepository();

        IAnimal mockLion = mockery.Stub<IAnimal>();
        IAnimal mockRhino = mockery.Stub<IAnimal>();

        IZoo zoo = new Zoo();

        zoo.CageThe(mockLion);
        zoo.CageThe(mockRhino);

        List<IAnimal> animals = new List<IAnimal>(zoo.Animals);
        Assert.IsTrue(animals.Contains(mockLion));
        Assert.IsTrue(animals.Contains(mockRhino));
    }
}

回答

我不太明白我们希望如何在不是模拟对象的对象上设置模拟期望。另外,我们还设置了返回IList的期望,当编译器生成迭代器时,这实际上不会发生。

如果要专门测试迭代器,则可能应该

Assert.IsNotNull(zoo.Animals);

然后验证枚举器实际上枚举了我们添加到Zoo中的所有东西。这就是我要去的地方。 :)

我不确定是否可以测试是否调用yield return,因为yield return只是编译器生成的IEnumerable的语法糖。例如,打电话

zoo.Animals.GetEnumerator();

不会执行我们在枚举器中编写的任何代码。第一次发生是在第一次调用IEnumerator.MoveNext();时。

现在,如果我们要测试具体的Zoo和该Zoo包含的IEnumerable之间的交互,则应将IEnumerable设置为Zoo上的一个字段,并将模拟IEnumerable注入该字段,而不是直接在Zoo中实现一个具体的IEnumerable。

我希望这会有所帮助。