使用 Trial 为 Twisted 代码编写测试¶
Trial 基础¶
Trial 是 Twisted 的测试框架。它提供了一个用于编写测试用例的库,以及用于在测试中使用 Twisted 环境的实用函数,以及用于运行测试的命令行工具。Trial 基于 Python 标准库的 unittest
模块。有关 Trial 如何查找测试的更多信息,请参阅 loadModule
文档。
要运行所有 Twisted 测试,请执行以下操作
$ python -m twisted.trial twisted
有关其他命令行选项,请参阅 Trial 手册页。
Trial 目录¶
您可能会注意到,在 Trial 完成测试后,当前工作目录中出现了一个新的 _trial_temp
文件夹。此文件夹是 Trial 进程的工作目录。单元测试可以使用它,并且可以随意将数据写入磁盘,而无需担心污染当前工作目录。
如果从同一个目录并行运行两个 Trial 实例,则会创建名为 _trial_temp-<counter>
的文件夹,以避免为两个不同的测试运行提供相同的临时目录。
使用 twisted.python.lockfile
实用程序来锁定 _trial_temp
目录。在 Linux 上,这会导致指向 pid 的符号链接。在 Windows 上,将使用包含 pid 作为内容的单个文件创建目录。如果 Trial 正常退出,这些锁定文件将被清理,否则它们将被保留。它们应该在 Trial 下次尝试使用它们锁定的目录时被清理,但也可以根据需要手动删除它们。
Twisted 特定的怪癖:reactor、Deferred、callLater¶
标准 Python unittest
框架(Trial 衍生自该框架)非常适合测试具有相当线性控制流的代码。Twisted 是一个异步网络框架,它提供了一种干净、合理的方式来建立响应事件(如计时器和传入数据)而运行的函数,从而创建高度非线性的控制流。Trial 有一些扩展可以帮助测试这种类型的代码。本节提供了一些关于如何使用这些扩展以及如何最好地构建测试的提示。
保持 Reactor 的状态不变¶
Trial 在单个进程中使用单个 reactor 运行整个测试套件(超过四千个测试)。因此,重要的是您的测试要将 reactor 保持在与发现它时相同的状态。剩余的计时器可能会在其他人的不知情测试中过期。剩余的连接尝试可能会完成(并失败)在后面的测试中。这些会导致间歇性故障,这些故障会在测试之间徘徊,并且非常耗时才能追踪到。
如果您的测试在 reactor 中留下了事件源,Trial 将会使测试失败。 tearDown
方法是放置清理代码的好地方:它始终运行,无论您的测试通过还是失败(就像 try-except-finally 结构中的 finally
子句)。 tearDown
中的异常将被标记为错误,并使测试失败。 TestCase.addCleanup
是另一个用于清理的有用工具。使用它,您可以注册可调用对象以在测试分配资源时清理资源。通常,代码应该编写成,只有在测试中分配的资源才需要在测试中清理。由实现内部分配的资源应该由实现清理。
如果您的代码使用 Deferred 或依赖于正在运行的 reactor,您可以从测试方法、setUp 或 tearDown 返回一个 Deferred,Trial 将会做正确的事情。也就是说,它会为您运行 reactor,直到 Deferred 触发并且其回调已运行。不要在测试中使用 reactor.run()
、 reactor.stop()
、 reactor.crash()
或 reactor.iterate()
。
对 reactor.callLater
的调用会创建 IDelayedCall
。这些需要在测试期间运行或取消,否则它们将比测试存活更久。这将很糟糕,因为它们可能会干扰后面的测试,导致无关测试出现令人困惑的故障!出于这个原因,Trial 会检查 reactor,以确保在测试结束后 reactor 中没有剩余的 IDelayedCall
,如果存在,则会使测试失败。确保这一切正常工作的最干净、最简单的方法是从测试中返回一个 Deferred。
类似地,在测试期间创建的套接字应该在测试结束时关闭。这适用于监听端口和客户端连接。因此,对 reactor.listenTCP
(以及 listenUNIX
等)的调用会返回 IListeningPort
,这些应该在测试结束前通过调用其 stopListening
方法进行清理。对 reactor.connectTCP
的调用会返回 IConnector
,这些应该通过调用其 disconnect
方法进行清理。Trial 会警告未关闭的套接字。
黄金法则:如果您的测试调用返回 Deferred 的函数,您的测试应该返回 Deferred。
使用计时器检测失败的测试¶
测试通常会设置某种故障安全超时机制,以防出现意外情况,并且没有遵循任何正常的测试失败路径,该机制将终止测试。此超时机制为测试可以消耗的时间设置了上限,并防止单个测试导致整个测试套件停滞。这对于 Twisted 测试套件尤其重要,因为它在每次更改提交到 Git 存储库时都会由 buildbot 自动运行。
在 Trial 中,可以通过在单元测试方法上设置 .timeout
属性来实现此功能。将属性设置为希望在测试引发超时错误之前经过的秒数。Trial 具有默认超时机制,即使未设置 timeout
属性,也会应用该机制。Trial 默认超时机制通常足够,只有在特殊情况下才需要覆盖它。
在测试中与警告交互¶
Trial 包含专门支持与 Python 的 warnings
模块交互。此支持允许以测试驱动的方式编写发出警告的代码,就像编写任何其他代码一样。它还改进了在运行测试套件时报告警告的方式。
TestCase.flushWarnings
允许编写测试,这些测试对在特定测试方法期间发出的警告进行断言。为了使用 flushWarnings
测试警告,请编写一个测试,该测试首先调用将发出警告的代码,然后调用 flushWarnings
并对结果进行断言。例如
class SomeWarningsTests(TestCase):
def test_warning(self):
warnings.warn("foo is bad")
self.assertEqual(len(self.flushWarnings()), 1)
在测试中未刷新的警告将被默认报告程序包含在其输出中,位于测试结果之后。如果 Python 的警告过滤器系统(参见 Python 的 -W 命令选项)配置为将警告视为错误,则未刷新的警告将导致测试失败,并将包含在默认报告程序的摘要部分中。请注意,与通常的操作不同,当 warnings.warn
作为测试方法的一部分被调用时,如果警告已被配置为错误,它不会引发异常。但是,如果在测试方法之外调用它(例如,在测试模块中的模块范围或由测试模块导入的模块中),则它 *将* 引发异常。
并行测试¶
在许多情况下,如果允许您的单元测试并行运行,您的单元测试可能会运行得更快,这样阻塞 I/O 调用将允许其他测试继续。Trial 与 unittest 一样,支持 -j 参数。运行 trial -j 3
以同时运行 3 个测试运行器。
这需要在创建测试时小心。显然,您需要确保您的代码在其他方面能够在并行模式下工作,同时在 Twisted 中工作……如果您在某些地方使用了奇怪的全局变量,并行测试可能会揭示这一点。
但是,如果您有一个测试在 setUp
函数中启动了外部数据库上的模式,在测试中对其进行了一些操作,然后在 tearDown 函数中删除了该模式,那么当测试相互踩踏时,您的测试将以不可预测的方式运行,因为它们有自己的模式。这实际上并不能表明您的代码存在真正的错误,而仅仅是测试特定的竞争条件。