1

给定以下简单的服务类,在GetCategories()方法中,您应该测试该categoryRepository.Query()方法被调用的事实,还是应该设置一个测试来保存类别列表并返回它们?

我想我要说的是,一旦覆盖了这个测试用例,就会嘲笑categoryRepository并验证它的方法是否被调用?Query

public class CategoryService : ValidatingServiceBase, ICategoryService
{
    private readonly IRepository<Category> categoryRepository;
    private readonly IRepository<SubCategory> subCategoryRepository;
    private readonly IValidationService validationService;

    public CategoryService(
        IRepository<Category> categoryRepository,
        IRepository<SubCategory> subCategoryRepository,
        IValidationService validationService)
        : base(validationService)
    {
        this.categoryRepository = categoryRepository;
        this.subCategoryRepository = subCategoryRepository;
        this.validationService = validationService;
    }

    public IEnumerable<Category> GetCategories()
    {
        return categoryRepository.Query().ToList();
    }
}

样品测试

[Fact]
public void GetCategories_Should_CallRepositoryQuery()
{
    var categoryRepo = new Mock<IRepository<Category>>();
    var service = new CategoryService(categoryRepo.Object, null, null);

    service.GetCategories();

    categoryRepo.Verify(x => x.Query(), Times.Once());
}
4

3 回答 3

4

没关系。在这两种情况下(模拟 + 行为验证vs存根 + 断言),您都会获得完全相同的结果,并且需要完全相同级别的类内部工作细节。坚持你认为在给定场景中更适合的那个。


您发布的单元测试是行为验证的一个示例。您不断言任何值,而是检查是否调用了某个方法。当方法调用没有可见的结果(考虑日志记录)或不返回任何值(显然)时,这尤其有用。它当然有缺点,尤其是当您对确实返回值的方法进行此类验证时,并且不检查它(就像您的情况一样 - 我们会处理它)。

存根和断言方法使用协作者来产生价值。它不检查是否调用了方法(至少不是直接调用,但是当您设置存根并且该设置有效时会执行此类测试),而是依赖于存根值的正确流。

让我们来看一个简单的例子。假设你测试你的类的一个方法,PizzaFactory.GetPizza它看起来像这样:

public Pizza GetPizza()
{
    var dough = doughFactory.GetDough();
    var cheese = ingredientsFactory.GetCheese();
    var pizza = oven.Bake(dough, cheese);
    return pizza;
}

通过行为验证,您将检查是否doughFactory.GetDough被调用,然后ingredientsFactory.GetCheese和最后oven.Bake。如果确实进行了这样的调用,您会假设创建了比萨饼。您不检查您的工厂是否返回比萨饼,但假设所有流程步骤都已完成,就会发生这种情况。您已经可以看到我之前提到的那个缺点 - 我可以调用所有正确的方法,但返回其他内容,例如:

var dough = doughFactory.GetDough();
var cheese = ingredientsFactory.GetCheese();
var pizza = oven.Bake(dough, cheese);
return garbageBin.FindPizza();

不是你点的披萨?请注意,所有对协作者的正确调用都像我们假设的那样发生。

使用stub + assert方法,除了验证你有存根之外,它看起来都很相似。您使用早期合作者生成的值来存根后来的合作者(如果不知何故你得到了错误的面团奶酪,烤箱将不会返回我们想要的披萨)。最终值是您的方法返回的值,这就是我们所断言的:

doughFactoryStub.Setup(df => dg.GetDough).Return("thick");
ingredientsFactoryStub.Setup(if => if.GetCheese()).Return("double");
var expectedPizza = new Pizza { Name = "Margherita" };
ovenStub.Setup(o => o.Bake("thick", "double")).Return(expectedPizza);

var actualPizza = pizzaFactory.GetPizza();

Assert.That(actualPizza, Is.EqualTo(expectedPizza));

如果过程的任何部分失败(比如doughFactory返回正常面团),那么最终值将不同并且测试将失败。

再一次,在我看来,在您的示例中,您使用哪种方法并不重要。在任何正常环境中,这两种方法都将验证同一件事,并且需要对您的实现具有相同水平的知识。为了更加安全,您可能更喜欢使用stub + assert方法,以防有人为您种植垃圾箱1。但是如果发生这种情况,单元测试是你的最后一个问题。


1但请注意,这可能不是故意的(尤其是在考虑复杂方法时)。

于 2013-09-03T22:48:09.957 回答
1

是的,那就是这样。

mockCategoryRepository.Setup(r => r.Query()).Returns(categories)
var actualCategories = new CategoryService(mockCategoryRepository, mock..).GetCategories();
CollectionAssert.AreEquivalent(categories, actualCategories.ToList());

它看起来与 Moq 和 NUnit 类似。

于 2013-09-03T22:47:19.673 回答
1

您介绍的是白盒测试——单元测试中也可以使用这种方法,但仅推荐用于简单方法。

在 Sruti 提供的答案中,该服务在黑盒意义上进行了测试。有关内部方法的知识仅用于准备测试,但您无法验证该方法是否被调用了一次、10 次或根本没有被调用。就个人而言,我验证方法调用只是为了验证某些必须存根的外部 API 是否正确使用(例如:发送电子邮件)。通常不关心方法的工作原理就足够了,只要它产生正确的结果。

使用黑盒测试,代码和测试更易于维护。对于白盒测试,在类的重构过程中,一些内部结构的大部分变化通常都必须跟随测试代码的变化。在黑盒方法中,您可以更自由地重新排列所有内容,并且仍然可以确保界面的外部行为没有改变。

于 2013-09-03T23:03:16.590 回答