在单元测试中模拟依赖项有什么好处

本文关键字:什么 依赖 单元测试 模拟 | 更新日期: 2023-09-27 17:55:39

我正在为我的控制器和服务层(C#,MVC)进行单元测试。我正在使用 Moq dll 在单元测试中模拟真实/依赖对象。

但是我对嘲笑依赖项或真实对象有点困惑。让我们举一个以下单元测试方法的例子:-

[TestMethod]
public void ShouldReturnDtosWhenCustomersFound_GetCustomers ()
{
    // Arrrange 
    var name = "ricky";
    var description = "this is the test";
    // setup mocked dal to return list of customers
    // when name and description passed to GetCustomers method
    _customerDalMock.Setup(d => d.GetCustomers(name, description)).Returns(_customerList);
    // Act
    List<CustomerDto> actual = _CustomerService.GetCustomers(name, description);
    // Assert
    Assert.IsNotNull(actual);
    Assert.IsTrue(actual.Any());
    // verify all setups of mocked dal were called by service
    _customerDalMock.VerifyAll();
}

在上面的单元测试方法中,我正在模拟 GetCustomers 方法并返回客户列表。这已经定义过了。如下所示:

List<Customer> _customerList = new List<Customer>
{
    new Customer { CustomerID = 1, Name="Mariya",Description="description"},
    new Customer { CustomerID = 2, Name="Soniya",Description="des"},
    new Customer { CustomerID = 3, Name="Bill",Description="my desc"},
    new Customer { CustomerID = 4, Name="jay",Description="test"},
};

让我们来看看客户模拟对象的断言和实际对象的断言:-

Assert.AreEqual(_customer.CustomerID, actual.CustomerID);
Assert.AreEqual(_customer.Name, actual.Name);
Assert.AreEqual(_customer.Description, actual.Description);

但是在这里我不明白它(在单元测试之上)总是工作正常。意味着我们只是在测试(在断言中)我们通过了哪些或我们正在返回(在模拟对象中)。我们知道真实/实际对象将始终返回我们传递的列表或对象。

那么在这里做单元测试或模拟的意义是什么呢?

在单元测试中模拟依赖项有什么好处

嘲笑的真正目的是实现真正的隔离。

假设您有一个CustomerService类,这取决于CustomerRepository。您编写了一些单元测试,涵盖了 CustomerService 提供的功能。他们都过去了。

一个月后,进行了一些更改,突然间您的CustomerServices单元测试开始失败 - 您需要找到问题所在。

所以你假设:

因为测试 CustomerServices 的单元测试失败了,所以问题一定出在该类中!!

右?错!问题可能出在CustomerServices上,也可能在于它的任何方面,即CustomerRepository.如果它的任何依赖项失败,则被测试的类也可能失败。

现在想象一个巨大的依赖链:A取决于BB取决于C,... Y取决于Z.如果在 Z 中引入了错误,则所有单元测试都将失败。

这就是为什么您需要将所测试的类与其依赖项(可能是域对象、数据库连接、文件资源等)隔离开来的原因。您想要测试一个单元

你的例子太简单了,无法炫耀嘲笑的真正好处。这是因为被测试的逻辑除了返回一些数据之外并没有真正做太多事情。

但是想象一下,作为一个例子,你的逻辑根据挂钟时间做了一些事情,比如每小时安排一些进程。在这种情况下,模拟时间源可以让你实际对这样的逻辑进行单元测试,这样你的测试就不必运行几个小时,等待时间过去。

除了已经说过的:

我们可以拥有没有依赖关系的类。我们唯一拥有的是没有模拟和存根的单元测试。

当我们有依赖关系时,
有几种:

  • 我们类主要以"即发即弃"的方式使用的服务,即不影响消费代码控制流的服务。

我们可以模拟这些(以及所有其他类型的)服务来测试它们被正确调用(集成测试)或只是为了注入,因为我们的代码可能需要它们。

  • 提供结果但没有内部的双向服务状态,并且不影响系统的状态。它们可以称为复杂的数据转换。

通过模拟这些服务,您可以测试您对服务实现的不同变体的代码行为的期望,而无需将所有服务都提升。

  • 影响系统状态或依赖于现实世界的服务现象或你无法控制的事情。'@500 - 内部服务器错误'给出了时间服务的一个很好的例子。

通过嘲笑,你可以让时间以任何需要的速度(和方向)流动。
另一个例子是使用 DB。在单元测试时,通常希望不要更改数据库状态,因为功能测试是不正确的。对于这种服务,"孤立"是嘲笑的主要(但不是唯一)动机。

  • 具有代码所依赖的内部状态的服务。

考虑实体框架:
SaveChanges()被召唤时,幕后会发生许多事情。EF 检测更改并修正导航属性。此外,EF 不允许你添加具有相同键的多个实体。
显然,模拟这种依赖关系的行为和复杂性可能非常困难......但通常如果它们设计得很好,你就没有了。如果您严重依赖某些组件提供的功能,则几乎无法替代此依赖项。可能需要的是再次隔离。你不想在测试时留下痕迹,因此黄油方法是告诉 EF 不要使用真正的 DB。是的,依赖关系不仅仅意味着一个接口。更多时候,它不是方法签名,而是预期行为的契约。例如,IDbConnection具有Open()Close()方法,这意味着某些调用序列。

当然,这不是严格的分类。最好将其视为极端。

@dcastro写道: You want to test a unit. 然而,这句话并没有回答你是否应该这样做的问题。
我们不要忽视集成测试。有时知道系统的某些复合部分出现故障是可以的。
例如,对于@dcastro给出的依赖链,我们可以尝试通过以下方式找到袋子可能的位置:

假设Z是最终依赖项。我们创建没有模拟的单元测试。所有边界条件都是已知的。100%的覆盖率是必须的。之后我们说 Z 工作正常。如果Z失败,我们的单元测试必须指出它。
模拟来自工程。在建造飞机时,没有人测试每个螺钉和螺栓。
统计方法用于确定地证明生产细节的工厂工作正常。

另一方面,对于系统的非常关键的部分,花时间和模拟依赖项的复杂行为是合理的。是的,它越复杂,测试的可维护性就越低。在这里,我宁愿将它们称为规范检查。
是的,您的 API 和测试都可能是错误的,但代码审查和其他形式的测试可以在一定程度上确保代码的正确性。一旦这些测试在进行某些更改后失败,您要么需要更改规格和相应的测试,要么找到错误并用测试覆盖案例。

我强烈建议您观看罗伊的视频:http://youtube.com/watch?v=fAb_OnooCsQ

在这种情况下,模拟允许您伪造数据库连接,以便您可以在原地和内存中运行测试,而无需依赖任何其他资源,即数据库。此测试断言,当调用服务时,将调用相应的 DAL 方法。

但是,列表的后续断言和列表中的值不是必需的。正如您正确注意到的那样,您只是断言您"模拟"的值被返回。这在模拟框架本身中很有用,可以断言模拟方法的行为符合预期。但是在你的代码中只是多余的。

在一般情况下,模拟允许人们:

  • 测试行为(当发生某些事情时,执行特定方法)
  • 虚假资源(例如,电子邮件服务器、Web 服务器、HTTP API 请求/响应、数据库)

相比之下,没有模拟的单元测试通常允许您测试状态。也就是说,在调用特定方法时,可以检测对象状态的变化。

所有先前的答案都假设嘲笑具有某种价值,然后他们继续解释该值应该是什么。

为了可能得出这个问题的

后代希望满足他们对这个问题的哲学反对意见,这里有一个不同的意见:

嘲笑,尽管是一个漂亮的把戏,但应该(几乎)不惜一切代价避免。

当你模拟被测代码的依赖关系时,根据定义,你是在做出两种假设:

  • 关于依赖项行为的假设
  • 关于待测代码内部工作的假设

可以说,关于依赖行为的假设是无辜的,因为它们只是根据某些需求或规范文档对真正的依赖应该如何行为的规定。我愿意接受这一点,并指出它们仍然是假设,每当你做出假设时,你就会过着危险的生活。

现在,不能争论的是,你对待测试代码的内部工作原理所做的假设本质上是将你的测试变成了一个白盒测试:模拟期望被测代码使用特定的参数对其依赖项发出特定的调用,并且当模拟返回特定结果时,被测代码应该以特定的方式运行。

如果您正在构建高关键性(航空航天级)软件,则白盒测试可能很合适,其目标是绝对不留任何机会,并且成本不是问题。它比黑盒测试的劳动强度高出几个数量级,因此非常昂贵,对于商业软件来说,这完全是矫枉过正,其目标只是满足要求,而不是确保内存中的每个位在任何给定时刻都有确切的期望值。

白盒测试是劳动密集型的,因为它使测试变得非常脆弱:每次修改被测代码时,即使修改不是为了响应需求的变化,你也必须修改你编写的每一个模拟来测试该代码。这是一个疯狂的高维护水平。

如何避免模拟和黑盒测试

  • 使用假货而不是模拟
    • 有关差异的解释,您可以阅读Martin Fowler的这篇文章:https://martinfowler.com/bliki/TestDouble.html 但举个例子,内存数据库可以用作假数据库来代替成熟的RDBMS。(请注意,假货比假货少得多。
    • 假货会给你和模拟一样多的隔离,但没有所有有风险和代价高昂的假设,最重要的是,没有所有的脆弱性。
  • 执行集成测试而不是单元测试
    • 当然,尽可能使用假货。
有关

有关我对该主题的想法的较长文章,请参阅 https://blog.michael.gr/2021/12/white-box-vs-black-box-testing.html