Skip to content

Latest commit

 

History

History
1245 lines (854 loc) · 58.7 KB

File metadata and controls

1245 lines (854 loc) · 58.7 KB

十、测试驱动开发

测试驱动开发TDD)是一种生产高质量软件的简单技术。它在 Python 社区中广泛使用,但在其他社区中也非常流行。

由于 Python 的动态特性,测试在 Python 中尤其重要。它缺少静态类型,所以在代码运行并执行每一行之前,不会注意到很多错误,甚至是一分钟的错误。但问题不仅在于 Python 中的类型如何工作。请记住,大多数错误与错误的语法使用无关,而是与可能导致重大故障的逻辑错误和细微误解有关。

本章分为两部分:

  • 我不测试,它提倡 TDD,并快速描述了如何使用标准库进行测试
  • 我做测试,这是为那些实践测试并希望从中获得更多的开发人员准备的

我不测试

如果您已被说服使用 TDD,则应转到下一节。它将集中于先进的技术和工具,使您的生活更容易时,与测试工作。这一部分主要针对那些不使用这种方法的人,并试图提倡使用这种方法。

测试驱动开发原则

测试驱动开发过程以其最简单的形式由三个步骤组成:

  1. 为尚未实现的新功能或改进编写自动化测试。
  2. 提供通过所有定义测试的最少代码。
  3. 重构代码以满足所需的质量标准。

关于这个开发周期,需要记住的最重要的事实是,应该在实现之前编写测试。对于没有经验的开发人员来说,这不是一项容易的任务,但它是确保您将要编写的代码是可测试的唯一方法。

例如,一位开发人员被要求编写一个函数来检查给定的数字是否为素数,他写了几个关于如何使用该数字以及预期结果的示例:

assert is_prime(5)
assert is_prime(7)
assert not is_prime(8)

实现该特性的开发人员不需要是唯一负责提供测试的人。这些例子也可以由其他人提供。例如,网络协议或加密算法的官方规范通常提供旨在验证实现正确性的测试向量。这些是测试用例的完美基础。

在此基础上,可以实现该功能,直到前面的示例起作用:

def is_prime(number):
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

错误或意外结果是函数应该能够处理的新用法示例:

>>> assert not is_prime(1)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AssertionError

可以相应地更改代码,直到新测试通过:

def is_prime(number):
    if number in (0, 1):
        return False

    for element in range(2, number):
        if number % element == 0:
            return False

    return True

更多的案例表明,实施仍然不完整:

>>> assert not is_prime(-3) 
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AssertionError

更新后的代码如下:

def is_prime(number):
    if number < 0 or number in (0, 1):
        return False

    for element in range(2, number):
        if number % element == 0:
            return False

    return True

从那里,所有测试都可以收集到一个测试函数中,该函数在每次代码演化时运行:

def test_is_prime():
    assert is_prime(5)
    assert is_prime(7)

    assert not is_prime(8)
    assert not is_prime(0)
    assert not is_prime(1)

    assert not is_prime(-1)
    assert not is_prime(-3)
    assert not is_prime(-6)

每次我们提出一个新的需求,test_is_prime()函数应该首先更新,以定义is_prime()函数的预期行为。然后,运行测试以检查实现是否提供了所需的结果。只有当已知测试失败时,才需要更新测试函数的代码。

测试驱动开发提供了很多好处:

  • 它有助于防止软件回归
  • 它提高了软件质量
  • 它提供了一种代码行为的低级文档
  • 它允许您在较短的开发周期内更快地生成健壮的代码

处理测试的最佳约定是将所有测试集合在单个模块或包(通常称为tests)中,并使用单个 shell 命令轻松运行整个套件。幸运的是,不需要自己构建整个测试工具链。Python 标准库和 Python 包索引都附带了大量的测试框架和实用程序,允许您以方便的方式构建、发现和运行测试。我们将在本章后面讨论此类软件包和模块的最显著示例。

防止软件回归

在开发人员的生活中,我们都面临着软件回归问题。软件回归是由变更引入的新缺陷。在项目开发过程中,当已知在以前版本的软件中工作的特性或功能被破坏并在某个时候停止工作时,它就会显现出来。

回归的主要原因是软件的高度复杂性。在某些情况下,不可能猜测代码库中的单个更改会导致什么结果。更改某些代码可能会破坏某些其他功能,有时会导致恶意的副作用,例如悄悄地破坏数据。而高复杂性不仅仅是庞大的代码库问题。当然,代码量与其复杂性之间存在明显的相关性,但即使是小项目(几百分之一/数千行代码)也可能具有复杂的体系结构,因此很难预测相对较小的更改的所有后果。

为了避免回归,每次发生更改时都应该测试软件提供的整套功能。如果没有这一点,您就无法可靠地区分始终存在于软件中的 bug 与刚刚正常工作的部件中引入的新 bug 之间的区别。

向几个开发人员开放一个代码库会放大这个问题,因为每个人都不会完全了解所有的开发活动。虽然版本控制系统可以防止冲突,但并不能防止所有不需要的交互。

TDD 有助于减少软件回归。每次更改后,整个软件都可以自动测试。只要每个特性都有一组适当的测试,这将起作用。当 TDD 正确完成时,testbase 与代码库一起增长。

由于完整的测试活动可以持续相当长的时间,因此将其委托给一些可以在后台执行工作的连续集成系统是一种很好的做法。我们已经在第 8 章管理代码中讨论了这些解决方案。然而,测试的本地重新启动也应该由开发人员手动执行,至少对于相关模块是如此。仅仅依靠持续集成将对开发人员的生产力产生负面影响。程序员应该能够在他们的环境中轻松地运行测试选择。这就是为什么您应该为项目仔细选择测试工具的原因。

提高代码质量

当一个新的模块、类或函数被编写时,开发人员将重点放在如何编写它以及如何产生他或她所能产生的最佳代码上。但是,当他或她专注于算法时,他或她可能会失去用户的观点:他或她的函数将如何以及何时使用?参数是否易于使用且符合逻辑?API 的名称正确吗?

这是通过应用前面章节中描述的技巧来实现的,例如第 4 章选择好名字。但是,有效地做到这一点的唯一方法是编写使用示例。此时,开发人员意识到自己编写的代码是否符合逻辑且易于使用。通常,第一次重构发生在模块、类或函数完成之后。

编写测试是代码的用例,有助于获得用户的观点。因此,开发人员在使用 TDD 时通常会生成更好的代码。测试巨大的函数和巨大的单片类是很困难的。考虑到测试而编写的代码倾向于以更干净和模块化的方式进行架构。

提供最好的开发者文档

测试是开发者学习软件如何工作的最佳场所。它们是代码主要为之创建的用例。阅读它们可以快速深入地了解代码的工作原理。有时候一个例子胜过千言万语。

这些测试始终与代码库保持同步,这一事实使它们成为软件中最好的开发人员文档。测试不会像文档那样过时,否则会失败。

更快地生成健壮代码

在没有测试的情况下编写会导致长时间的调试会话。一个模块中的错误可能会在软件的完全不同的部分表现出来。因为你不知道该怪谁,你花了大量的时间调试。当测试失败时,最好一次解决一个小错误,因为您可以更好地了解真正的问题在哪里。测试通常比调试更有趣,因为它是编码。

如果您将修复代码所花费的时间与编写代码所花费的时间一起度量,那么它通常会比 TDD 方法所花费的时间更长。当您开始一段新代码时,这并不明显。这是因为建立测试环境和编写前几个测试所花费的时间与编写前几段代码所花费的时间相比非常长。

但是有些测试环境确实很难设置。例如,当您的代码与 LDAP 或 SQL server 交互时,编写测试一点也不明显。这在本章的仿冒品和仿冒品章节中有介绍。

什么样的测试?

有几种测试可以在任何软件上进行。主要是验收测试(或功能测试)和单元测试,这些都是大多数人在讨论软件测试话题时想到的。但是,您可以在项目中使用一些其他类型的测试。我们将在本节中简要讨论其中的一些问题。

验收试验

验收测试将重点放在特性上,并像黑匣子一样处理软件。它只是确保软件真正做到它应该做的事情,使用与用户相同的媒体并控制输出。这些测试通常在开发周期外编写,以验证应用程序是否满足需求。它们通常作为软件的检查表运行。通常,这些测试不是通过 TDD 完成的,而是由经理、QA 人员甚至客户构建的。在这种情况下,它们通常被称为用户验收测试

尽管如此,它们可以而且应该用 TDD 原则来完成。可以在编写特性之前提供测试。开发人员得到一堆验收测试,通常由功能规范组成,他们的工作是确保代码通过所有测试。

用于编写这些测试的工具取决于软件提供的用户界面。Python 开发人员使用的一些流行工具有:

|

应用程序类型

|

工具

| | --- | --- | | Web 应用程序 | Selenium(用于带有 JavaScript 的 Web UI) | | Web 应用程序 | zope.testbrowser(不测试 JS) | | WSGI 应用程序 | paste.test.fixture(不测试 JS) | | Gnome 桌面应用程序 | 狗尾草 | | Win32 桌面应用程序 | pywinauto |

对于功能测试工具的广泛列表,GrigGhorghiu 在维护了一个 wiki 页面 https://wiki.python.org/moin/PythonTestingToolsTaxonomy

单元测试

单元测试是非常适合测试驱动开发的低级测试。顾名思义,他们专注于测试软件单元。软件单元可以理解为应用程序代码中最小的可测试部分。根据应用程序的不同,从整个模块到单个方法或函数的大小可能有所不同,但通常单元测试是为尽可能小的代码片段编写的。单元测试通常将被测试单元(模块、类、函数等)与应用程序的其余部分和其他单元隔离开来。当需要外部依赖项时,例如 web API 或数据库,它们通常会被伪对象或模拟替代。

功能测试

功能测试关注于整体特性和功能,而不是小代码单元。它们的目的与验收试验相似。主要区别在于功能测试不一定需要使用与用户相同的界面。例如,在测试 web 应用程序时,一些用户交互(或其后果)可以通过合成 HTTP 请求或直接数据库访问来模拟,而不是模拟真实的页面加载和鼠标单击。

这种方法通常比使用用户验收测试中使用的工具进行测试更简单、更快。有限功能测试的缺点是,它们往往无法覆盖应用程序中不同抽象层和组件相遇的足够部分。集中在集合点上的测试通常称为集成测试。

集成测试

集成测试代表比单元测试更高级别的测试。他们测试更大的部分代码,并将重点放在许多应用程序层或组件相遇并相互交互的情况下。集成测试的形式和范围因项目的架构和复杂性而异。例如,在小型和整体式项目中,这可能与运行更复杂的功能测试一样简单,并允许它们与真正的支持服务(数据库、缓存等)交互,而不是模仿或伪造它们。对于由多个服务构建的复杂场景或产品,真正的集成测试可能非常广泛,甚至需要在反映生产的大型分布式环境中运行整个项目。

集成测试通常与功能测试非常相似,它们之间的边界非常模糊。集成测试也在逻辑上测试单独的功能和特性,这是非常常见的。

负载和性能测试

负载测试和性能测试提供有关代码效率的客观信息,而不是其正确性。负载测试和性能测试的术语被一些人互换使用,但第一个术语实际上指的是性能的一个有限方面。负载测试侧重于测量代码在某些人为需求(负载)下的行为。这是一种非常流行的测试 web 应用程序的方法,其中负载被理解为来自真实用户或编程客户端的 web 流量。需要注意的是,负载测试往往覆盖对应用程序的整个请求,因此与集成和功能测试非常相似。这使得确保已测试的应用程序组件被充分验证正常工作非常重要。性能测试通常是所有旨在测量代码性能的测试,甚至可以针对较小的代码单元。因此,负载测试只是性能测试的一个特定子类型。

它们是一种特殊的测试,因为它们不提供二进制结果(失败/成功),而只提供一些性能质量度量。这意味着需要解释单个结果和/或与不同测试运行的结果进行比较。在某些情况下,项目需求可能会对代码设置一些困难的时间或资源限制,但这并不能改变这样一个事实,即在这些类型的测试方法中总是涉及一些任意的解释。

负载性能测试是开发任何需要满足某些服务****级别协议的软件的一个很好的工具,因为它有助于降低影响关键代码路径性能的风险。无论如何,它不应该被过度使用。

代码质量测试

代码质量没有任意的尺度,可以确定它是好是坏。不幸的是,代码质量的抽象概念不能用数字的形式来衡量和表达。但是,我们可以测量已知与代码质量高度相关的软件的各种度量。举几个例子:

  • 代码样式冲突的数量
  • 文件量
  • 复杂性度量,如 McCabe 的圈复杂度
  • 静态代码分析警告的数量

许多项目在其持续集成工作流中使用代码质量测试。好的和流行的方法是至少测试基本指标(静态代码分析和代码风格冲突),而不允许将任何代码合并到主流中,从而降低这些指标。

Python 标准测试工具

Python 在标准库中提供了两个主要模块来编写测试:

单元测试

unittest基本上提供了 JUnit 对 Java 的功能。它提供了一个名为TestCase的基类,它有一组广泛的方法来验证函数调用和语句的输出。

创建此模块是为了编写单元测试,但只要测试使用用户界面,也可以使用它编写验收测试。例如,一些测试框架提供了帮助来驱动工具,例如在unittest之上的 Selenium。

使用unittest为模块编写一个简单的单元测试是通过子类化TestCase并使用test前缀编写方法完成的。测试驱动开发原则部分的最后一个示例如下所示:

import unittest

from primes import is_prime

class MyTests(unittest.TestCase):
    def test_is_prime(self):
        self.assertTrue(is_prime(5))
        self.assertTrue(is_prime(7))

        self.assertFalse(is_prime(8))
        self.assertFalse(is_prime(0))
        self.assertFalse(is_prime(1))

        self.assertFalse(is_prime(-1))
        self.assertFalse(is_prime(-3))
        self.assertFalse(is_prime(-6))

if __name__ == "__main__":
    unittest.main()

unittest.main()功能是使整个模块作为测试套件可执行的实用程序:

$ python test_is_prime.py -v
test_is_prime (__main__.MyTests) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

unittest.main()函数扫描当前模块的上下文并查找子类TestCase的类。它实例化它们,然后运行以test前缀开头的所有方法。

好的测试套件遵循通用且一致的命名约定。例如,如果primes.py模块中包含is_prime函数,则可以调用PrimesTests测试类并将其放入test_primes.py文件中:

import unittest

from primes import is_prime

class PrimesTests(unittest.TestCase):
    def test_is_prime(self):
        self.assertTrue(is_prime(5))
        self.assertTrue(is_prime(7))

        self.assertFalse(is_prime(8))
        self.assertFalse(is_prime(0))
        self.assertFalse(is_prime(1))

        self.assertFalse(is_prime(-1))
        self.assertFalse(is_prime(-3))
        self.assertFalse(is_prime(-6))

if __name__ == '__main__':
    unittest.main()

从那里开始,utils模块每次进化,test_utils模块都会得到更多的测试。

为了工作,test_primes模块需要在上下文中提供primes模块。这可以通过将两个模块都放在同一个包中,或者通过向 Python 路径显式添加一个测试模块来实现。实际上,setuptoolsdevelop命令在这里非常有用。

在整个应用程序上运行测试的前提是,您有一个脚本,可以用所有测试模块构建测试活动unittest提供一个TestSuite类,该类可以聚合测试并将它们作为测试活动运行,只要它们都是TestCaseTestSuite的实例。

在 Python 的过去,有一种惯例,即测试模块提供了一个test_suite函数,该函数返回__main__部分中使用的TestSuite实例,该实例由命令提示符调用,或者由测试运行程序使用:

import unittest

from primes import is_prime

class PrimesTests(unittest.TestCase):
    def test_is_prime(self):
        self.assertTrue(is_prime(5))

        self.assertTrue(is_prime(7))

        self.assertFalse(is_prime(8))
        self.assertFalse(is_prime(0))
        self.assertFalse(is_prime(1))

        self.assertFalse(is_prime(-1))
        self.assertFalse(is_prime(-3))
        self.assertFalse(is_prime(-6))

class OtherTests(unittest.TestCase):
    def test_true(self):
        self.assertTrue(True)

def test_suite():
    """builds the test suite."""
    suite = unittest.TestSuite()
    suite.addTests(unittest.makeSuite(PrimesTests))
    suite.addTests(unittest.makeSuite(OtherTests))

    return suite

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')

从 shell 运行此模块将打印测试活动输出:

$ python test_primes.py -v
test_is_prime (__main__.PrimesTests) ... ok
test_true (__main__.OtherTests) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

unittest模块没有适当的测试发现实用程序时,在较早版本的 Python 中需要上述方法。通常,所有测试的运行都由一个全局脚本完成,该脚本浏览代码树以查找测试并运行它们。这被称为测试发现,本章稍后将更广泛地介绍。现在,您应该只知道,unittest提供了一个简单的命令,可以从带有test前缀的模块和包中发现所有测试:

$ python -m unittest -v
test_is_prime (test_primes.PrimesTests) ... ok
test_true (test_primes.OtherTests) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

如果使用上述命令,则不需要手动定义__main__部分并调用unittest.main()函数。

博士测试

doctest是一个模块,它以交互提示会话的形式从 docstring 或文本文件中提取片段并回放,以检查示例输出是否与真实输出相同。

例如,包含以下内容的文本文件可以作为测试运行:

Check addition of integers works as expected::

>>> 1 + 1
2

假设此文档文件以test.rst名称存储在文件系统中。doctest模块提供了一些从这些文档中提取和运行测试的功能:

>>> import doctest
>>> doctest.testfile('test.rst', verbose=True)
Trying:
 1 + 1
Expecting:
 2
ok
1 items passed all tests:
 1 tests in test.rst
1 tests in 1 items.
1 passed and 0 failed.
Test passed.
TestResults(failed=0, attempted=1)

使用doctest有很多优点:

  • 可以通过示例记录和测试包
  • 文档示例始终是最新的
  • 使用 doctests 中的示例编写包有助于维护用户的观点

然而,doctest 并没有使单元测试过时;它们只能用于在文档中提供人类可读的示例。换句话说,当测试涉及低级问题或需要复杂的测试夹具(可能会混淆文档)时,不应使用它们。

一些 Python 框架,比如 Zope,广泛使用 doctest,并且它们有时会受到代码新手的批评。有些博士真的很难阅读和理解,因为这些例子打破了技术写作的一条规则,它们不能在简单的提示下进行,它们需要广泛的知识。因此,本应帮助新手的文档很难阅读,因为代码示例(通过 TDD 构建的 doctest)基于复杂的测试装置甚至特定的测试 API。

正如第 9 章记录您的项目中所述,当您使用作为软件包文档一部分的博士测试时,请注意遵守技术写作的七条规则。

在这个阶段,您应该对 TDD 带来的好处有一个很好的概述。如果您仍然不相信,您应该尝试几个模块。使用 TDD 编写一个包,测量构建、调试和重构所花费的时间。你应该很快发现它确实是优越的。

我做测试

如果您来自我不测试部分,并且现在确信要进行测试驱动开发,那么恭喜您!您知道测试驱动开发的基础知识,但是在您能够有效地使用这种方法之前,您还需要学习更多的东西。

本节描述了开发人员在编写测试时遇到的一些问题以及解决这些问题的一些方法。它还提供了 Python 社区中流行的测试运行程序和工具的快速回顾。

单元测试陷阱

unittest模块是在 Python2.1 中引入的,此后开发人员大量使用了该模块。但是社区中的一些人创建了一些替代测试框架,他们对unittest的弱点和局限性感到失望。

这些是经常被提出的常见批评:

  • 框架使用量大是因为:
    • 您必须在TestCase的子类中编写所有测试
    • 您必须在方法名称前加上test
    • 我们鼓励您使用TestCase中提供的断言方法,而不是简单的assert语句,并且现有的方法可能不会涵盖所有用例
  • 该框架很难扩展,因为它需要大量的基类子类化或装饰器之类的技巧。
  • 测试夹具有时很难组织,因为setUptearDown设施与TestCase级别绑定,尽管它们每次测试运行一次。换句话说,如果测试夹具涉及许多测试模块,那么组织其创建和清理并不简单。
  • 在 Python 软件上运行测试活动并不容易。默认的测试运行程序(python -m unittest确实提供了一些测试发现,但没有提供足够的过滤功能。实际上,必须编写额外的脚本来收集测试、聚合测试,然后以方便的方式运行测试。

编写测试时需要一种更轻巧的方法,而不必忍受一个看起来太像它的老 Java 兄弟 JUnit 的框架的僵化。由于 Python 不需要使用 100%基于类的环境,因此最好提供一个更具 Python 风格的测试框架,它不基于子类。

一个共同的办法是:

  • 提供一种将任何函数或任何类标记为测试的简单方法
  • 通过插件系统扩展框架
  • 为所有测试级别提供一个完整的测试夹具环境:整个活动、模块级别和测试级别的一组测试
  • 为基于测试发现的测试运行程序提供一组广泛的选项

单元测试替代方案

一些第三方工具试图通过以unittest扩展的形式提供额外的特性来解决上述问题。

PythonWiki 提供了一个很长的各种测试实用程序和框架列表(请参阅https://wiki.python.org/moin/PythonTestingToolsTaxonomy ),但只有两个项目特别受欢迎:

鼻子

nose主要是一款具有强大发现功能的测试跑步者。它有广泛的选项,允许在 Python 应用程序中运行所有类型的测试活动。

它不是标准库的一部分,但可在 PyPI 上使用,并且可以使用 pip 轻松安装:

pip install nose

测试转轮

安装 nose 后,在提示符处会出现一个名为nosetests的新命令。可以直接使用它运行本章第一节中介绍的测试:

nosetests -v
test_true (test_primes.OtherTests) ... ok
test_is_prime (test_primes.PrimesTests) ... ok
builds the test suite. ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.009s

OK

nose通过递归浏览当前目录并自行构建测试套件来发现测试。乍一看,前面的示例与简单的python -m unittest相比没有任何改进。如果您使用--help开关运行此命令,则差异会很明显。您会注意到,nose 提供了数十个参数,允许您控制测试发现和执行。

写作测试

nose更进一步,运行所有名称与正则表达式((?:^|[b_.-])[Tt]est)匹配的类和函数,这些正则表达式位于与之匹配的模块中。大致上,所有以test开头并位于与模式匹配的模块中的可调用项也将作为测试执行。

例如,此test_ok.py模块将被nose识别并运行:

$ more test_ok.py
def test_ok():
 print('my test')
$ nosetests -v
test_ok.test_ok ... ok

-----------------------------------------------------------------
Ran 1 test in 0.071s

OK

同时执行常规的TestCase类和doctests类。

最后,nose提供了类似于TestCase方法的断言函数。但这些函数是作为遵循 PEP 8 命名约定的函数提供的,而不是使用unittest使用的 Java 约定(请参阅http://nose.readthedocs.org/ )。

编写测试夹具

nose支持三个级别的夹具:

  • 包级:可以在包含所有测试模块的测试包的__init__.py模块中增加setupteardown功能
  • 模块级:一个测试模块可以有自己的setupteardown功能
  • 测试级别:可调用项也可以使用提供的with_setup装饰器具有 fixture 功能

例如,要在模块和测试级别设置测试夹具,请使用以下代码:

def setup():
    # setup code, launched for the whole module
    ...

def teardown():
    # teardown code, launched for the whole module
    ... 

def set_ok():
    # setup code launched only for test_ok
    ...

@with_setup(set_ok)
def test_ok():
    print('my test')

与 setuptools 和插件系统集成

最后,nosesetuptools平滑集成,因此test命令可以与之配合使用(python setup.py test。此集成通过在setup.py脚本中添加test_suite元数据完成:

setup(
    #...
    test_suite='nose.collector',
)

nose还使用setuptool's入口点机制供开发人员编写nose插件。这允许您覆盖或修改工具的各个方面,从测试发现到输出格式。

nose插件列表维护在https://nose-plugins.jottit.com

收尾

nose是一个完整的测试工具,修复了unittest的许多问题。它仍然被设计为在测试中使用隐式前缀名称,这对一些开发人员来说仍然是一个限制。虽然可以自定义此前缀,但它仍然需要遵循约定。

这种配置声明的约定不错,比unittest中要求的锅炉铭牌代码要好得多。但是,例如,使用显式修饰符可能是摆脱test前缀的一种好方法。

此外,通过插件扩展nose的能力使其非常灵活,允许开发人员定制工具以满足其需求。

如果您的测试工作流需要覆盖许多 nose 参数,那么您可以轻松地在主目录或项目根目录中添加一个.nosercnose.cfg文件。它将指定nosetests命令的默认选项集。例如,一个好的实践是在测试运行期间自动查找 doctest。启用运行 doctests 的nose配置文件示例如下:

[nosetests]
with-doctest=1
doctest-extension=.txt

py 试验

py.testnose非常相似。事实上,后者的灵感来自py.test,因此我们将主要关注使这些工具彼此不同的细节。该工具是作为一个名为py的更大软件包的一部分诞生的,但现在它们是单独开发的。

与本书中提到的每一个第三方软件包一样,py.test在 PyPI 上可用,并且可以将pip作为pytest安装:

$ pip install pytest

从那里,一个新的py.test命令在提示符处可用,可以像nosetests一样使用。该工具使用类似的模式匹配和测试发现算法来捕获要运行的测试。该图案比nose使用的图案更为严格,只能捕捉:

  • Test开头的类,在以test开头的文件中
  • test开头的函数,在以test开头的文件中

注意使用正确的字符大小写。如果函数以大写字母“T”开头,它将被视为一个类,因此被忽略。如果一个类以小写字母“t”开头,py.test将中断,因为它将尝试将其作为函数处理。

py.test的优点是:

  • 能够轻松禁用某些测试类
  • 一种灵活且新颖的夹具处理机制
  • 在多台计算机之间分配测试的能力

编写测试夹具

py.test支持两个机构处理夹具。第一个是根据 xUnit 框架建模的,类似于nose。当然,语义有点不同。py.test将在每个测试模块中查找三个级别的夹具,如下例所示:

def setup_module(module): 
    """ Setup up any state specific to the execution 
        of the given module.
    """

def teardown_module(module):    
    """ Teardown any state that was previously setup
        with a setup_module method.
    """

def setup_class(cls):    
    """ Setup up any state specific to the execution
        of the given class (which usually contains tests).
    """

def teardown_class(cls):    
    """ Teardown any state that was previously setup
        with a call to setup_class.
    """

def setup_method(self, method):
    """ Setup up any state tied to the execution of the given
        method in a class. setup_method is invoked for every
        test method of a class.
    """

def teardown_method(self, method):
    """ Teardown any state that was previously setup
        with a setup_method call.
    """

每个函数将获取当前模块、类或方法作为参数。因此,测试夹具将能够在上下文上工作,而无需像nose一样查找上下文。

使用py.test编写 fixture 的另一种机制是基于依赖注入的概念,允许以更模块化和可伸缩的方式维护测试状态。非 xUnit 样式的装置(设置/拆卸过程)始终具有唯一的名称,需要通过在类中的测试函数、方法和模块中声明它们的使用来显式激活。

fixture 最简单的实现形式是使用pytest.fixture()修饰符声明的命名函数。要将夹具标记为测试中使用的夹具,需要将其声明为函数或方法参数。为了使它更清楚,考虑前一个例子的测试模块,用于使用 Tyle T2E.夹具改写的 OutT1 函数。

import pytest

from primes import is_prime

@pytest.fixture()
def prime_numbers():
    return [3, 5, 7]

@pytest.fixture()
def non_prime_numbers():
    return [8, 0, 1]

@pytest.fixture()
def negative_numbers():
    return [-1, -3, -6]

def test_is_prime_true(prime_numbers):
    for number in prime_numbers:
        assert is_prime(number)

def test_is_prime_false(non_prime_numbers, negative_numbers):
    for number in non_prime_numbers:
        assert not is_prime(number)

    for number in non_prime_numbers:
        assert not is_prime(number)

禁用测试功能和类

py.test提供了一种简单的机制,可以在特定条件下禁用某些测试。这称为跳过,pytest包为此提供了.skipif装饰程序。如果在某些条件下需要跳过单个测试函数或整个测试类修饰符,则需要使用此修饰符定义它,并提供一些值来验证是否满足预期条件。以下是官方文档中跳过在 Windows 上运行整个测试用例类的示例:

import pytest

@pytest.mark.skipif(
    sys.platform == 'win32',
    reason="does not run on windows"
)
class TestPosixCalls:

    def test_function(self):
        """will not be setup or run under 'win32' platform"""

当然,您可以预定义跳过条件,以便在您的测试模块中共享它们:

import pytest

skipwindows = pytest.mark.skipif(
    sys.platform == 'win32',
    reason="does not run on windows"
)

@skip_windows
class TestPosixCalls:

    def test_function(self):
        """will not be setup or run under 'win32' platform"""

如果以这种方式标记测试,则根本不会执行它。然而,在某些情况下,您希望运行这样的测试并希望执行它,但您知道,在已知条件下,它可能会失败。为此,提供了不同的装饰器。它是@mark.xfail并确保测试始终运行,但如果出现预定义的条件,它应该在某个点失败:

import pytest

@pytest.mark.xfail(
sys.platform == 'win32',
    reason="does not run on windows"
)
class TestPosixCalls:

    def test_function(self):
        """it must fail under windows"""

使用xfailskipif严格得多。测试始终执行,如果在预期的时间内没有失败,则整个py.test运行将导致失败。

自动化分布式测试

py.test一个有趣的特性是它能够在多台计算机上分发测试。只要可以通过 SSH 访问计算机,py.test就可以通过发送要执行的测试来驱动每台计算机。

然而,这一功能依赖于网络;如果连接断开,从属设备将无法继续工作,因为它完全由主设备驱动。

当项目有长时间的测试活动时,Buildbot 或其他持续集成工具更可取。但是当您在一个需要耗费大量资源来运行测试的应用程序上工作时,py.test分布式模型可以用于测试的临时分发。

收尾

py.test与非常相似,因为不需要样板代码来聚合其中的测试。它还有一个很好的插件系统,在 PyPI 上有大量的扩展。

最后,py.test专注于使测试运行速度更快,并且与该领域的其他工具相比确实更优越。另一个值得注意的特性是 fixture 的原始方法,它确实有助于管理可重用的 fixture 库。有些人可能会争辩说,这涉及到太多的魔法,但它确实简化了测试套件的开发。py.test的这一单一优势使它成为我的首选工具,因此我真的推荐它。

测试覆盖率

代码覆盖率是一个非常有用的指标,它提供了有关项目代码测试效果的客观信息。它只是测量在所有测试执行期间执行的代码行数和行数。它通常表示为百分比,100%覆盖率意味着每一行代码都是在测试期间执行的。

最流行的代码覆盖工具称为 simply coverage,可在 PyPI 上免费获得。用法非常简单,只包含两个步骤。第一步是在 shell 中运行 coverage run 命令,并将运行所有测试的脚本/程序的路径作为参数:

$ coverage run --source . `which py.test` -v
===================== test session starts ======================
platformdarwin -- Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1 -- /Users/swistakm/.envs/book/bin/python3
cachedir: .cache
rootdir: /Users/swistakm/dev/book/chapter10/pytest, inifile: 
plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0
collected 6 items 

primes.py::pyflakes PASSED
primes.py::pep8 PASSED
test_primes.py::pyflakes PASSED
test_primes.py::pep8 PASSED
test_primes.py::test_is_prime_true PASSED
test_primes.py::test_is_prime_false PASSED

========= 6 passed, 1 pytest-warnings in 0.10 seconds ==========

coverage run 还接受-m参数,该参数指定一个可运行的模块名,而不是程序路径,对于某些测试框架来说,该路径可能更好:

$ coverage run -m unittest
$ coverage run -m nose
$ coverage run -m pytest

下一步是根据.coverage文件中兑现的结果生成代码覆盖率的可读报告。coverage软件包支持几种输出格式,最简单的一种只是在终端中打印 ASCII 表:

$ coverage report
Name             StmtsMiss  Cover
------------------------------------
primes.py            7      0   100%
test_primes.py      16      0   100%
------------------------------------
TOTAL               23      0   100%

另一种有用的覆盖率报告格式是 HTML,可在 web 浏览器中浏览:

$ coverage html

此 HTML 报告的默认输出文件夹是您工作目录中的htmlcov/coverage html输出的真正优势在于,您可以浏览项目的注释源,其中突出显示了缺少测试覆盖的部分(如图 1所示):

Testing coverage

图 1 coverage HTML 报告中带注释的源示例

您应该记住,尽管您应该始终努力确保 100%的测试覆盖率,但这并不能保证代码得到完美的测试,也不能保证代码在任何地方都会被破坏。这只意味着在执行过程中每一行代码都已到达,但不一定测试了所有可能的条件。在实践中,确保完整的代码覆盖可能相对容易,但要确保每个代码分支都被覆盖确实很难。对于可能有多种组合的if语句和特定语言结构(如list/dict/set理解)的函数的测试尤其如此。您应该始终关注良好的测试覆盖率,但决不应将其度量作为测试套件质量的最终答案。

假冒伪劣

编写单元测试的前提是隔离正在测试的代码单元。测试通常向函数或方法提供一些数据,并验证其返回值和/或其执行的副作用。这主要是为了确保测试:

  • 涉及应用程序的原子部分,可以是函数、方法、类或接口
  • 提供确定性的、可重复的结果

有时,程序组件的适当隔离并不明显。例如,如果代码发送电子邮件,它可能会调用 Python 的smtplib模块,该模块将通过网络连接与 SMTP 服务器一起工作。如果我们希望我们的测试是可复制的,并且只是测试电子邮件是否具有所需的内容,那么这可能不应该发生。理想情况下,单元测试应该在没有外部依赖和副作用的任何计算机上运行。

由于 Python 的动态特性,可以使用monkey patching将测试夹具中的运行时代码(即在运行时动态修改软件而不接触源代码)修改为第三方代码或库的行为。

打造假货

测试中的虚假行为可以通过发现被测试代码与外部部件工作所需的最小交互集来创建。然后,手动返回输出或使用以前记录的真实数据池。

这是通过启动一个空类或函数并将其用作替换来实现的。然后启动测试,并迭代更新伪代码,直到其行为正确为止。由于 Python 类型系统的特性,这是可能的。只要对象的行为与预期的类型相同,并且不需要通过子类化成为其祖先,就认为该对象与给定类型兼容。这种在 Python 中进行类型化的方法称为 duck 类型化。如果某个对象的行为类似于 duck,则可以将其视为 duck。

让我们以一个名为mailer的模块中名为send的函数为例,该模块发送电子邮件:

import smtplib
import email.message

def send(
    sender, to,
    subject='None',
    body='None',
    server='localhost'
):
    """sends a message."""
    message = email.message.Message()
    message['To'] = to
    message['From'] = sender
    message['Subject'] = subject
    message.set_payload(body)

    server = smtplib.SMTP(server)
    try:
        return server.sendmail(sender, to, message.as_string())
    finally:
        server.quit()

py.test将用于演示本节中的假货和模拟品。

对应的测试可以是:

from mailer import send

def test_send():
    res = send(
        'john.doe@example.com', 
        'john.doe@example.com', 
        'topic',
        'body'
    )
    assert res == {}

只要本地主机上有 SMTP 服务器,此测试就会通过并正常工作。否则,它将失败如下:

$ py.test --tb=short
========================= test session starts =========================
platform darwin -- Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1
rootdir: /Users/swistakm/dev/book/chapter10/mailer, inifile: 
plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0
collected 5 items 

mailer.py ..
test_mailer.py ..F

============================== FAILURES ===============================
______________________________ test_send ______________________________
test_mailer.py:10: in test_send
 'body'
mailer.py:19: in send
 server = smtplib.SMTP(server)
.../smtplib.py:251: in __init__
 (code, msg) = self.connect(host, port)
.../smtplib.py:335: in connect
 self.sock = self._get_socket(host, port, self.timeout)
.../smtplib.py:306: in _get_socket
 self.source_address)
.../socket.py:711: in create_connection
 raise err
.../socket.py:702: in create_connection
 sock.connect(sa)
E   ConnectionRefusedError: [Errno 61] Connection refused
======== 1 failed, 4 passed, 1 pytest-warnings in 0.17 seconds ========

可以向 SMTP 类添加修补程序:

import smtplib
import pytest
from mailer import send

class FakeSMTP(object):
    pass

@pytest.yield_fixture()
def patch_smtplib():
    # setup step: monkey patch smtplib
    old_smtp = smtplib.SMTP
    smtplib.SMTP = FakeSMTP

    yield

    # teardown step: bring back smtplib to 
    # its former state
    smtplib.SMTP = old_smtp

def test_send(patch_smtplib):
    res = send(
        'john.doe@example.com',
        'john.doe@example.com',
        'topic',
        'body'
    )
    assert res == {}

在前面的代码中,我们使用了一个新的pytest.yield_fixture()装饰器。它允许我们使用生成器语法在单个夹具功能中提供设置和拆卸过程。现在,我们的测试套件可以使用补丁版本的smtplib再次运行:

$ py.test --tb=short -v
======================== test session starts ========================
platform darwin -- Python 3.5.1, pytest-2.8.7, py-1.4.31, pluggy-0.3.1 -- /Users/swistakm/.envs/book/bin/python3
cachedir: .cache
rootdir: /Users/swistakm/dev/book/chapter10/mailer, inifile: 
plugins: capturelog-0.7, codecheckers-0.2, cov-2.2.1, timeout-1.0.0
collected 5 items 

mailer.py::pyflakes PASSED
mailer.py::pep8 PASSED
test_mailer.py::pyflakes PASSED
test_mailer.py::pep8 PASSED
test_mailer.py::test_send FAILED

============================= FAILURES ==============================
_____________________________ test_send _____________________________
test_mailer.py:29: in test_send
 'body'
mailer.py:19: in send
 server = smtplib.SMTP(server)
E   TypeError: object() takes no parameters
======= 1 failed, 4 passed, 1 pytest-warnings in 0.09 seconds =======

正如我们从前面的记录中看到的,我们的FakeSMTP类实现还没有完成。我们需要更新其接口以匹配原始 SMTP 类。根据 duck typing 原则,我们只需要提供被测试send()功能所需的接口:

class FakeSMTP(object):
    def __init__(self, *args, **kw):
        # arguments are not important in our example
        pass

    def quit(self):
        pass

    def sendmail(self, *args, **kw):
        return {}

当然,伪类可以通过新的测试来提供更复杂的行为。但它应该尽可能简短。同样的原理也可以用于更复杂的输出,通过记录这些输出,通过伪 API 将其返回。这通常是针对第三方服务器(如 LDAP 或 SQL)执行的。

重要的是要知道,在对任何内置或第三方模块进行修补时,应特别小心。如果处理不当,这种方法可能会留下不必要的副作用,在测试之间传播。幸运的是,许多测试框架和工具都提供了适当的实用程序,使任何代码单元的修补都安全且容易。在我们的示例中,我们手动完成了所有操作,并提供了一个自定义的patch_smtplib()夹具功能,其中包含单独的设置和拆卸步骤。py.test中的典型解决方案要简单得多。该框架带有内置的 monkey patch fixture,可以满足我们的大部分配线需求:

import smtplib
from mailer import send

class FakeSMTP(object):
    def __init__(self, *args, **kw):
        # arguments are not important in our example
        pass

    def quit(self):
        pass

    def sendmail(self, *args, **kw):
        return {}

def test_send(monkeypatch):
    monkeypatch.setattr(smtplib, 'SMTP', FakeSMTP)

    res = send(
        'john.doe@example.com',
        'john.doe@example.com',
        'topic',
        'body'
    )
    assert res == {}

你应该知道赝品有真正的局限性。如果你决定伪造一个外部依赖,你可能会引入真正的服务器不会有的 bug 或不想要的行为,反之亦然。

使用模拟

模拟对象是通用的假对象,可用于隔离测试代码。它们自动化了对象输入和输出的构建过程。静态类型语言中更多地使用模拟对象,其中猴子补丁比较困难,但它们在 Python 中仍然很有用,可以缩短代码以模拟外部 API。

Python 中有很多可用的模拟库,但最受认可的是unittest.mock,它是在标准库中提供的。它是作为第三方软件包创建的,不是 Python 发行版的一部分,但很快就作为临时的软件包包含在标准库中(请参阅https://docs.python.org/dev/glossary.html#term-临时 api)。对于早于 3.3 的 Python 版本,您需要从 PyPI 安装它:

pip install Mock

在我们的示例中,使用unittest.mock来修补 SMTP 比从头开始创建一个假的 SMTP 要简单得多:

import smtplib
from unittest.mock import MagicMock
from mailer import send

def test_send(monkeypatch):
    smtp_mock = MagicMock()
    smtp_mock.sendmail.return_value = {}

    monkeypatch.setattr(
        smtplib, 'SMTP', MagicMock(return_value=smtp_mock)
    )

    res = send(
        'john.doe@example.com',
        'john.doe@example.com',
        'topic',
        'body'
    )
    assert res == {}

模拟对象或方法的return_value参数允许您定义调用返回的值。使用模拟对象时,每次代码调用属性时,它都会动态地为属性创建一个新的模拟对象。因此,没有提出任何例外。例如,我们前面编写的quit方法就是这种情况,它不再需要定义了。

在前面的示例中,我们实际上创建了两个模拟:

  • 第一个模拟 SMTP 类对象而不是其实例的对象。这使您可以轻松创建新对象,而不必考虑预期的__init__()方法。如果视为可调用,mock 默认返回新的Mock()对象。这就是为什么我们需要提供另一个 mock 作为其return_value关键字参数来控制实例接口。
  • 模拟实际实例的第二个实例在经过修补的smtplib.SMTP()调用中返回。在这个模拟中,我们控制sendmail()方法的行为。

在我们的示例中,我们使用了py.test框架中提供的 monkey 补丁实用程序,但unittest.mock提供了自己的补丁实用程序。在某些情况下(如修补类对象),使用它们而不是特定于框架的工具可能更简单、更快。以下是使用unittest.mock模块提供的patch()上下文管理器进行猴子补丁的示例:

from unittest.mock import patch
from mailer import send

def test_send():
    with patch('smtplib.SMTP') as mock:
        instance = mock.return_value
        instance.sendmail.return_value = {}
        res = send(
            'john.doe@example.com',
            'john.doe@example.com',
            'topic',
            'body'
        )
        assert res == {}

测试环境及依赖兼容性

环境隔离的重要性在本书中已经多次提到。通过在应用程序级(虚拟环境)和系统级(系统虚拟化)上隔离执行环境,您可以确保测试在可重复的条件下运行。通过这种方式,您可以保护自己不受由断开的依赖关系引起的罕见而模糊的问题的影响。

允许适当隔离测试环境的最佳方法是使用支持系统虚拟化的良好连续集成系统。对于开源项目,比如 Travis CI(Linux 和 OS X)或 AppVeyor(Windows),有很好的免费解决方案,但是如果您需要这样的东西来测试专有软件,您很可能需要花费一些时间在现有的开源 CI 工具之上自己构建这样的解决方案(GitLab CI、Jenkins 和 Buildbot)。

依赖矩阵测试

开源 Python 项目的测试矩阵在大多数情况下只关注不同的 Python 版本,很少关注不同的操作系统。对于纯 Python 的项目,不在不同的系统上进行测试和构建是完全可以的,并且不存在预期的系统互操作性问题。但是有些项目,特别是作为编译的 Python 扩展分发时,应该在各种目标操作系统上进行测试。对于开源项目,您甚至可能被迫使用一些独立的 CI 系统来为三个最流行的系统(Windows、Linux 和 Mac OS X)提供构建。如果您正在寻找一个好的例子,您可以查看小型 pyrilla 项目(请参阅https://github.com/swistakm/pyrilla )这是 Python 的简单 C 音频扩展。它同时使用 Travis CI 和 AppVeyor,以便为 Windows 和 Mac OS X 以及大量 CPython 版本提供编译版本。

但测试矩阵的维度并没有在系统和 Python 版本上结束。经常提供与其他软件(如缓存、数据库或系统服务)集成的包应该在集成应用程序的各种版本上进行测试。tox(参考是一个使此类测试变得容易的好工具 http://tox.readthedocs.org )。它提供了一种简单的方法来配置多个测试环境,并使用一个tox命令运行所有测试。这是一个非常强大和灵活的工具,但也很容易使用。展示其用法的最佳方式是向您展示一个配置文件的示例,该配置文件实际上是 tox 的核心。这是来自 django userena 项目的tox.ini文件(参考https://github.com/bread-and-pepper/django-userena

[tox]
downloadcache = {toxworkdir}/cache/

envlist =
    ; py26 support was dropped in django1.7
    py26-django{15,16},
    ; py27 still has the widest django support
    py27-django{15,16,17,18,19},
    ; py32, py33 support was officially introduced in django1.5
    ; py32, py33 support was dropped in django1.9
    py32-django{15,16,17,18},
    py33-django{15,16,17,18},
    ; py34 support was officially introduced in django1.7
    py34-django{17,18,19}
    ; py35 support was officially introduced in django1.8
    py35-django{18,19}

[testenv]
usedevelop = True
deps =
    django{15,16}: south
    django{15,16}: django-guardian<1.4.0
    django15: django==1.5.12
    django16: django==1.6.11
    django17: django==1.7.11
    django18: django==1.8.7
    django19: django==1.9
    coverage: django==1.9
    coverage: coverage==4.0.3
    coverage: coveralls==1.1

basepython =
    py35: python3.5
    py34: python3.4
    py33: python3.3
    py32: python3.2
    py27: python2.7
    py26: python2.6

commands={envpython} userena/runtests/runtests.py userenaumessages {posargs}

[testenv:coverage]
basepython = python2.7
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
commands=
    coverage run --source=userena userena/runtests/runtests.py userenaumessages {posargs}
    coveralls

此配置允许您在五个不同版本的 Django 和六个版本的 Python 上测试django-userena。并非每个 Django 版本都能在每个 Python 版本上工作,tox.ini文件使定义此类依赖性约束相对容易。实际上,整个构建矩阵由 21 个独特的环境组成(包括用于代码覆盖率收集的特殊环境)。手动创建每个测试环境甚至使用 shell 脚本都需要付出巨大的努力。

Tox 很好,但是如果我们想改变测试环境中其他不是简单的 Python 依赖项的元素,那么它的用法就会变得更加复杂。这种情况下,我们需要在不同版本的系统包和支持服务下进行测试。解决此问题的最佳方法是再次使用良好的连续集成系统,该系统允许您轻松定义环境变量矩阵,并在虚拟机上安装系统软件。ianitor项目提供了一个使用 TravisCI 实现这一点的好例子(参考https://github.com/ClearcodeHQ/ianitor/ 已经在第 9 章中提到,记录您的项目。它是领事发现服务的一个简单实用程序。领事项目有一个非常活跃的社区,每年都会发布许多新版本的代码。这使得针对该服务的各种版本进行测试非常合理。这确保了ianitor项目仍然是该软件最新版本的最新版本,但也不会破坏与以前 Consor 版本的兼容性。以下是 Travis CI 的.travis.yml配置文件的内容,该文件允许您针对三个不同的 Consor 版本和四个 Python 解释器版本进行测试:

language: python

install: pip install tox --use-mirrors
env:
  matrix:
    # consul 0.4.1
    - TOX_ENV=py27     CONSUL_VERSION=0.4.1
    - TOX_ENV=py33     CONSUL_VERSION=0.4.1
    - TOX_ENV=py34     CONSUL_VERSION=0.4.1
    - TOX_ENV=py35     CONSUL_VERSION=0.4.1

    # consul 0.5.2
    - TOX_ENV=py27     CONSUL_VERSION=0.5.2
    - TOX_ENV=py33     CONSUL_VERSION=0.5.2
    - TOX_ENV=py34     CONSUL_VERSION=0.5.2
    - TOX_ENV=py35     CONSUL_VERSION=0.5.2

    # consul 0.6.4
    - TOX_ENV=py27     CONSUL_VERSION=0.6.4
    - TOX_ENV=py33     CONSUL_VERSION=0.6.4
    - TOX_ENV=py34     CONSUL_VERSION=0.6.4
    - TOX_ENV=py35     CONSUL_VERSION=0.6.4

    # coverage and style checks
    - TOX_ENV=pep8     CONSUL_VERSION=0.4.1
    - TOX_ENV=coverage CONSUL_VERSION=0.4.1

before_script:
  - wget https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip
  - unzip consul_${CONSUL_VERSION}_linux_amd64.zip
  - start-stop-daemon --start --background --exec `pwd`/consul -- agent -server -data-dir /tmp/consul -bootstrap-expect=1

script:
  - tox -e $TOX_ENV

前面的示例为ianitor代码提供了 14 个独特的测试环境(包括pep8coverage构建)。此配置还使用 tox 在 Travis VM 上创建实际测试虚拟环境。这实际上是将 tox 与不同 CI 系统集成的一种非常流行的方法。通过将尽可能多的测试环境配置转移到 tox,您减少了将自己锁定到单个供应商的风险。大多数 Travis CI 竞争对手都支持安装新服务或定义系统环境变量,因此,如果市场上有更好的产品,或者 Travis 将改变其开源项目的定价模式,那么切换到其他服务提供商应该相对容易。

文档驱动开发

与其他语言相比,Python 中的doctest是一个真正的优势。文档可以使用在测试时也可以运行的代码示例,这改变了 TDD 的实现方式。例如,文档的一部分可以在开发周期中通过doctests完成。这种方法还确保提供的示例始终是最新的,并且真正有效。

通过 doctest 而不是常规的单元测试构建软件称为文档驱动开发DDD。开发人员在实现代码时用简单的英语解释代码在做什么。

写故事

在 DDD 中编写博士测试是通过构建一段代码如何工作和应该如何使用的故事来完成的。这些原则是用简单的英语描述的,然后在本文中分发了一些代码使用示例。一个好的实践是开始编写关于代码如何工作的文本,然后添加一些代码示例。

要查看实践中的博士测试示例,让我们看看atomisator包(请参阅https://bitbucket.org/tarek/atomisator )。其atomisator.parser子包装(在packages/atomisator.parser/atomisator/parser/docs/README.txt下)的文件文本如下:

=================
atomisator.parser
=================

The parser knows how to return a feed content, with
the `parse` function, available as a top-level function::

>>> from atomisator.parser import Parser

This function takes the feed url and returns an iterator
over its content. A second parameter can specify a maximum
number of entries to return. If not given, it is fixed to 10::

>>> import os
>>> res = Parser()(os.path.join(test_dir, 'sample.xml'))
>>> res
<itertools.imap ...>

Each item is a dictionary that contain the entry::

>>> entry = res.next()
>>> entry['title']
u'CSSEdit 2.0 Released'

The keys available are:

>>> keys = sorted(entry.keys())
>>> list(keys)
    ['id', 'link', 'links', 'summary', 'summary_detail', 'tags', 
     'title', 'title_detail']

Dates are changed into datetime::

>>> type(entry['date'])
>>>

稍后,doctest 将根据新的元素或所需的更改进行演变。这个 doctest 对于想要使用这个包的开发人员来说也是一个很好的文档,应该根据这个用法进行更改。

在文档中编写测试的一个常见陷阱是将其转换为不可读的文本。如果发生这种情况,则不应再将其视为文档的一部分。

这就是说,一些专门通过 doctest 工作的开发人员通常将他们的 doctest 分为两类:可读和可用的 doctest,以便它们可以成为包文档的一部分;不可读的 doctest,仅用于构建和测试软件。

许多开发人员认为应该为后者放弃 doctest,而代之以常规的单元测试。其他人甚至使用专用的 doctest 来修复 bug。

因此,博士测试和常规测试之间的平衡是一个品味问题,取决于团队,只要博士测试的发布部分是可读的。

当 DDD 用于项目时,关注可读性,并决定哪些博士有资格成为已发布文档的一部分。

总结

本章提倡使用 TDD,并提供了以下方面的更多信息:

  • unittest陷阱
  • 第三方工具:nosepy.test
  • 如何制作赝品和仿制品
  • 文档驱动的开发

由于我们已经知道如何构建、打包和测试软件,在接下来的两章中,我们将重点介绍如何找到性能瓶颈并优化程序。