测试最佳实践

在为 Fuchsia 编写测试时,请确保您熟悉编写测试的测试原则测试范围

测试的理想属性

一般来说,只要不与测试的其他目标发生冲突,就可以使用以下属性。

  • 隔离:测试应与不在测试范围内的代码、系统和详细信息隔离。不同的测试用例应相互隔离。在 Fuchsia 上,我们使用组件框架提供的隔离保证来隔离测试。隔离测试的一个实用结果是,测试可以并行运行或按不同顺序运行,并且它们的结果相同。
  • 封闭:测试结果由测试内容定义。在 Fuchsia 上,如果单元测试或集成测试的结果由测试软件包的内容定义,则该测试或集成测试是封闭的。如果测试的软件包未更改,则可以假定测试的行为未发生变化,并且测试仍然通过或仍然失败。此属性可用于选择要运行哪些测试来验证给定更改。在无法保证封闭性的情况下,下一个最佳替代方案是运行所有测试,这会成本高昂。
  • 可重现:重新运行同一测试应产生相同的结果。 隔离性和封闭性可提高可重现性。测试范围越大,测试的可重现难度就越大。
  • 与被测代码的接近程度:测试应专注于特定的单元测试和测试,并控制不是被测试的内容。
  • 弹性佳:当被测代码更改时,测试无需更改,除非更改对代码的用途很重要。在对代码进行良性更改后继续运行的测试更具弹性。执行代码的公共 API 或其他形式的协定的测试往往更有弹性。当您专注于测试行为而不是实现时,也会发生这种情况。
  • 易于问题排查:测试失败时,应生成明确且可操作的错误,以便识别缺陷。如果用于隔离错误或范围更小的测试,那么在失败时通常更容易进行问题排查。
  • 速度快:测试通常是开发者反馈环的一部分。更快的反馈循环有助于提升开发者的工作效率。
  • 可靠:测试失败应表示受测代码存在真实缺陷。可靠的测试通过后会更有信心,并且产生的错误故障更少,维护成本也会更高。
  • 灵活:运行测试所受的限制越少,就越容易运行。在 Fuchsia 上,我们特别感谢能够尽可能在模拟器上运行测试。

测试的不良属性

通常不建议使用以下属性。根据情况的不同,它们可能是为测试目的而进行权衡的缺点,这总体上是净正例。

  • 不稳定:如果测试产生虚假失败,然后在重试时通过,则比较不稳定。不稳定测试的维护成本较高,由于重试而导致运行速度变慢,并且提供的结果置信度较低。
  • 运行缓慢:运行时间较长的测试会产生效率较低的反馈环,并降低开发者的工作效率。测试范围越大,通常运行速度越慢。
  • 难以排查问题:如果测试因出错而无法立即解决或因其他原因无法指明失败的根本原因而失败,则更难以进行问题排查。开发者必须查看测试失败本身之外的其他位置(例如系统日志或内部系统状态)来排查测试失败问题。
  • 更改检测器:如果被测代码以对外部观察者无害的方式更改,如果测试与对功能不重要的实现细节过于紧密地耦合,则测试往往会失败。变更检测器测试的维护成本更高。

针对接口和协定进行测试

建议:使用公共 API 和其他接口以及提供给被测代码的客户端的协定进行测试。这些测试对良性更改的适应能力更强。

不建议:不要测试对客户端不重要的实现细节。当被测代码以良性方式发生变化时,此类测试通常会中断。

深入阅读:

编写可读的测试代码

在编写测试时考虑可读性,就像编写被测代码时一样。

  • 如果测试的正文包含理解测试所需的所有信息,则表示测试完成。
  • 如果测试不包含任何其他干扰信息,即视为测试简洁

建议:编写完整且简洁的测试用例。更喜欢编写更多单个测试测试用例,并且每个测试用例都非常侧重于特定情形和问题。

不推荐:不要为了减少测试用例数而将多个场景合并成多个测试用例,

深入阅读:

编写可重现的确定性测试

测试应该是确定性的,也就是说,每次针对同一代码修订运行测试都会产生相同的结果。否则,测试的维护成本可能会很高。

线程处理或具有时效性的代码、随机数生成器 (RNG) 和跨组件通信是造成不确定性的常见来源。

建议:按照以下提示编写确定性测试:

  • 对于具有时效性的测试,请使用模拟时钟或模拟时钟来提供确定性。请参阅 fuchsia_async::Executor::new_with_fake_time虚假时钟
  • 线程代码必须始终使用正确的同步基元来避免不稳定性。请尽可能首选单线程测试。
  • 始终提供一种注入 RNG 种子的机制,并将其用于测试。
  • 在组件集成测试中使用模拟。请参阅 Realm Builder
  • 使用对不稳定行为敏感的测试时,请考虑多次运行测试,以确保测试始终通过。为此,您可以使用重复标志(例如 GoogleTest 中的 --gtest_repeat 和 Go 中的 --test.count)。如果您的测试在合并之前容易出现波动,请尽量在本地运行至少 100-1000 次。

不建议:切勿在测试中使用 sleep 作为弱同步的方法。在循环迭代之间,在循环中进行轮询时,您可以使用短暂的休眠。

测试替身:桩、模拟、虚假对象

测试替身代表测试期间被测代码的实际依赖项。

  • 是一个测试替身,会返回给定值且不包含任何逻辑。
  • mock 是一个测试替身,就应如何调用它设定了预期。模拟对于测试互动非常有用。
  • 虚构对象是真实对象的轻量级实现。

建议:为您拥有的代码创建虚假对象,以便客户端可以在自己的测试中将其用作测试替身。对于集成测试,请考虑使您在测试领域中的实际组件实例与系统的其余部分隔离开来,并记录此行为。

不建议:不要在测试中过度使用模拟,因为这样可能会导致创建质量不佳的测试,这些测试的可读性较低,维护成本也较高,但通过的置信度也会较低。请避免模拟并非您所有的依赖项。

深入阅读:

适当使用端到端测试

建议:使用端到端测试来测试关键用户历程。此类测试应以用户的身份执行相应历程,例如通过自动执行用户互动和检查界面状态更改。

不推荐:不要使用端到端测试来涵盖其他层或较小范围中缺失的测试,因为当这些端到端测试捕获错误时,将很难对其进行问题排查。

建议:谨慎使用端到端测试,作为平衡测试策略的一部分,该策略更多地依赖于能够快速运行并产生精确且具有实用价值的结果的较小范围测试。

不建议:不要在开发反馈周期中依赖于端到端测试,因为这些测试通常需要运行很长时间,并且通常比范围较小的测试产生的结果更加不稳定。

深入阅读: