模拟及其对软件设计的影响
#网络开发人员 #php #测试 #体系结构

在谈论单元测试时,一个人也会自动谈论模拟和模拟依赖的需求。

这似乎是对沿着“嘲笑正在测试的班级的每个外部依赖性”的嘲讽的典型态度,这种态度可能非常危险。

因此,以下是一些谨慎的措施,即在您的代码库中大量使用模拟可以就整体系统设计或体系结构进行。

tl; dr

模拟用于在单元测试中提供隔离,因此应有助于创建易于测试和易于更改的软件。但是实际上,任何单位测试套件中的模拟都过度使用可以掩盖软件设计中的潜在问题,这些问题使软件难以测试难以更改。因此,应将模拟视为工具箱中的一种工具,可以用来构建高质量且可维护的软件。这意味着人们应该知道何时应用此特定工具,或者何时应该使用工具箱中的其他工具。

模拟很难重构

在广泛使用模拟时要谨慎,因为很难自动重构类别。这是因为IDES不能为被大量嘲笑的重构类提供强大的支持,而像PHPStan这样的工具可能无法有效地检测到这些与模拟相关的问题。

更广泛地说,很难确保模拟行为与实际实现相同/或预期的方式(尤其是在基础实现发生变化时)。

仅在需要的地方使用模拟,因为:

  • 创建对象很难,因为您需要大量的嵌套依赖项来创建对象。

  • 该类产生一些您在单位测试中不需要的副作用(例如,DB写入)。

对于所有其他情况,请使用实际实现,并尽可能地依赖于Phpunit的模拟框架的魔力。

专注于行为,而不是实施:有效的单位测试原则

严重依赖模型会在测试的单元测试中产生不好的模式 如何实现某物而不是 实现实际上是什么。如果测试以较重的方式实施,它们将与实施紧密结合,这意味着它们依赖于实现细节,并且在实现细节更改时可能会更频繁地失败,而不是在测试中的类别的实际行为更改时。考虑以下两个示例更改某些类:

之前:

$id = $this->repository->search($criteria, $context)->first()?->getId();

之后:

$id = $this->repository->searchIds($criteria, $context)->firstId();

之前:

$values = $this->connection->fetchAllAssociative('SELECT first, second FROM foo ...');
$values = $this->mapToKeyValue($values);

之后:

$values = $this->connection->fetchKeyValue('SELECT first, second FROM foo ...');

根据定义,这两个变化都是重构的纯粹示例:

重构是一种纪律处分的技术,用于重组现有的代码,改变其内部结构而不改变其外部行为。
- > Martin Fowler

但是,当单元测试嘲笑repositoryconnection依赖项时,即使外部行为(这是测试应真正测试的方法)也不会更改。

在某些情况下使用模拟可以使用模拟,但不是全部。

上面的示例可能是完全有效的示例(因为模拟的类依赖DB),这通常在现实生活中遇到。

此外,本文档的目的是让您了解使用模拟的缺点。

模拟可能表明您的班级没有精心设计

在设计良好且可测试的系统中,隔离单个类或模块并将它们区分开并区分包含核心业务逻辑的组件,该组件应经过广泛的单位测试,以及负责与该交互的部分外部环境和产生副作用。这些副作用元素应在单位测试中取代。实际上,建议进行集成测试,因为它们的主要目的是在测试中抽象和促进副作用。

当您应用Domain Driven DesignHexagonal Architecture(又名端口和适配器)的原理时,这种抽象就遵循。我在previous post中详细介绍了有关这些架构的考虑。

在现有代码库中缺少这种抽象是很难编写“好”单位测试的原因之一。

因此,在编写单元测试时,对模拟的严重依赖可以表明软件设计的潜在问题,这表明封装不足。因此,建议设计代码以促进更好的封装并减少对​​广泛模拟的需求。这可以提高可测试性和整体软件质量。

比模拟更好的选择

有更好的选择,但这取决于用例。这是一些替代方法:

  1. 使用真实的实现(这意味着真正的依赖性易于创建并且不产生副作用)

  2. 使用实际依赖性的手工制作的虚拟实现,这很容易配置,并且在该用例中的行为就像平台一样(这意味着实际依赖性可能需要以一种简单的方式设计替换)

  3. 使用模拟框架的后备(当真正的依赖性不容易更换时)

设计代码库的设计方式会直接影响您是否可以依靠选项1或选项2而无需求助于重型模拟。

结论:首先写测试!

首先编写测试时,上述大多数应该从开箱即用!

没有任何从测试开始的人会从配置模拟开始。

在我们提供有关此的见解时,必须验证信息。因此,我们鼓励您探索以下参考文献,以获得更深入的理解并形成自己的意见。

参考

Frank de Jonge关于完全相同的主题(PHP中有更多示例):https://blog.frankdejonge.nl/testing-without-mocking-frameworks/

马丁·福勒(Martin Fowler)关于模拟(选项3)和存根(选项2)之间的差异:https://martinfowler.com/articles/mocksArentStubs.html

Mathias Noback的演示文稿关于六角形体系结构:https://matthiasnoback.nl/talk/a-testing-strategy-for-hexagonal-applications/

关于PHP中的单位测试的一些良好现实示例:https://github.com/sarven/unit-testing-tips

一般对测试的出色文章:https://dannorth.net/2021/07/26/we-need-to-talk-about-testing/

相当古老的(1997!)关于不是使用代码覆盖范围的论文:http://www.exampler.com/testing-com/writings/coverage.pdf

很棒的博客文章系列如何避免模拟:https://philippe.bourgau.net/categories/#how-to-avoid-mocks-series