具有模拟存储库依赖性的单元测试类

时间:2018-01-24 15:26:11

标签: c# unit-testing mocking repository moq

我有一个包含多种方法的服务类。服务对存储库有一些依赖性。我正在使用Moq来模拟存储库。我有一个问题,正确的单位测试方法之一。我会举个例子:

public interface IRepository<T>
{
    IEnumerable<T> FindAll();
    IEnumerable<T> FindByQuery(Predicate<T> predicate);
    //many other methods for retreiving T's
}

public class MyService
{
    private readonly IRepository<Category> _repo;

    public MyService(IRepository<Category> repo)
    {
        _repo = repo;
    }

    public List<Category> FindActiveCategories()
    {
        return _repo.FindAll().Where(x => x.Active).ToList();
    }
}

现在,我写了一个单元测试:

public FindActiveCategories_WhenCalled_ShouldReturnActiveCategories() {
   var moq = new Mock<IRepository<Category>>();
   var list = new List<Category>
   {
       new Category {Active = true},
       new Category {Active = false}
   };
   moq.Setup(x => x.FindAll()).Returns(list);
   var service = new MyService(moq.Object);
   var result = service.FindActiveCategories();

   Assert.IsTrue(result.All(x=>x.Active));
}

当然,测试通过了。但是我意识到我使用FindAll修改了我的服务方法中的所有类别 - 这是一个显而易见的事情要纠正,因为我不想将几千个类别加载到内存中只是为了拉出它们中的少数几个。所以我将FindActiveCategories方法的实现改为:

public List<Category> FindActiveCategories()
{
    return _repo.FindByQuery(x => x.Active).ToList();
}

这次我的测试失败了。问题很明显 - 测试取决于实现细节。我知道FindActiveCategories方法使用了存储库的FindAll方法,所以我为这个方法编写了一个设置。更改实现后,我必须更改测试方法的实现 - 这似乎是一个问题。当然我可以设置所有的Find ...方法但是在存储库中有很多它们可以选择其中的很多,这对我来说似乎也不对。更不用说TDD了 - 如果我是先尝试编写测试,我不知道是什么以及如何模拟存储库接口。我的问题是:处理这种依赖关系能够编写与实现无关的单元测试的正确方法是什么。

2 个答案:

答案 0 :(得分:1)

尽管有这个问题的标题,但这里的实际问题似乎是“如果我的代码实现发生变化,我是否需要更改测试?”

答案是,如果实现完全包含在您测试的代码中,则为“否”。例如,如果我有一个方法来乘以两个值并以明显的方式实现它,我可以编写测试来证明它有效。现在,如果我改变实现以使用for循环进行计算并在循环中累积总计,那么我的所有测试都应该通过而不进行任何更改。

问题是我们编写的大多数代码都不是这样的:它取决于其他东西。如果被测代码依赖于其他东西,我们有两个选择:进行集成测试,或模拟依赖。

  • 集成测试为测试中的类提供了在实际使用时将使用的实际依赖项。这是最有价值的测试,因为它是完全现实的。这里的答案也是“如果你改变实施,就不需要改变测试”。但是,这通常是不切实际的,因为它会以指数方式增加编写和维护测试的成本。
  • 模拟需要您指定依赖项的行为方式,因此根据定义,您的测试必须知道要测试的代码将使用什么,以及如何使用。因此,如果您更改实施,您应该合理地期望必须更改测试,因此答案是“是”。

但是,请考虑这一点。测试中与模拟无关的部分不需要更改。您没有使用Arrange Act Assert模式(这有助于使测试更具可读性,尤其是在模拟时);但是Act和Assert部分,这将是你的测试的最后两行,不应该改变。也许这会让你相信你还在做TDD。

但是不要为此失眠:还有其他一些事情可以从你的注意力中获益更多。例如,将方法的实现更改为...

public List<Category> FindActiveCategories()
{
    return new List<Category>();
}

...然后测试将通过,即使代码不会执行您想要的操作(因为断言中的All对于空列表返回true)。

希望这很有用。

答案 1 :(得分:0)

以下内容适用于上述更改。

[TestClass]
public class MyServiceShould {
    [TestMethod]
    public void FindActiveCategories_WhenCalled_ShouldReturnActiveCategories() {
        //Arrange
        var moq = new Mock<IRepository<Category>>();
        var list = new List<Category> {
            new Category {Active = true},
            new Category {Active = false}
        };
        moq
            .Setup(x => x.FindByQuery(It.IsAny<Predicate<Category>>()))
            .Returns((Predicate<Category> predicate) => list.Where(x => predicate(x)));
        var service = new MyService(moq.Object);

        //Act
        var result = service.FindActiveCategories();

        //Assert
        Assert.IsTrue(result.All(x => x.Active));
    }
}

mock接受传递的谓词并将其应用于假集合,以便允许测试完成。

参考Moq Quickstart以更好地理解如何使用模拟框架。