测试/模拟什么

本文关键字:什么 模拟 测试 | 更新日期: 2023-09-27 18:30:08

给定以下简单的服务类,在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());
}

测试/模拟什么

这没关系。在这两种情况下(mock+行为验证stub+断言),您可以实现完全相同的结果,并且需要关于类内部工作的完全相同级别的详细信息。在给定的场景中,坚持选择你认为更适合的那个。


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

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

让我们举一个简单的例子。假设您测试类的一个方法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请注意,这可能不是故意的(尤其是在考虑复杂方法时)。

是的,就是这样。

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

Moq和NUnit的情况会类似。

您介绍的是一个白盒测试,这种方法在单元测试中也是可行的,但仅推荐用于简单的方法。

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

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