Python 高级编程(第2版)--第10章 测试驱动开发

测试驱动开发

测试驱动开发(Test-Driven Development, TDD)是一种生产高质量软件的简单技术。

不测试

测试开发的原则:

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

记住这个开发周期的最重要事情是,测试应该在实现之前编写。

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

  • 有助于防止软件回归。
  • 提高软件质量。
  • 提供了一种底层的代码行为文档。
  • 允许你在较短的开发周期中更快地写出健壮的代码。

处理测试的最佳约定是将它们放在一个模块或包(通常命名为 tests)中,并使用一个简单的 shell 命令运行整个套件。

防止软件回归

软件回归是由变化引入的一个新的 bug。这体现为一些功能和特性在以前版本的软件中正常工作,而在项目开发中的某个时间点上,它们被破坏了并且无法正常工作。

回归的主要原因是软件的高度复杂性。在某些时候,无法预测代码库中的单个更改可能导致什么问题。更改一些代码可能会破坏一些其他的功能,有时会导致恶性副作用,例如悄悄损坏数据。

为了避免回归,应该在每次发生变化时,对软件提供的整套功能进行测试。TDD 有助于减少软件回归。整个软件可以在每次更改后自动测试。

  • 提高代码质量。
    • 有效的做法是编写使用示例。此时此刻,开发人员就会意识到他或她写的代码是否合理且易于使用。通常,第一次重构发生在模块、类或函数完成之后。
    • 编写代码的测试用例,有助于从用户的角度思考问题。因此,开发人员在使用 TDD 时通常会写出更好的代码。
  • 提供最好的开发文档。
    • 测试是开发人员了解软件工作原理的最佳场所。它们是使用的实例,代码就是主要为它而建。阅读它们可以快速深入地了解代码的工作原理。
    • 事实上,这些测试应该始终与代码库一样保持更新,使它们成为一个软件可以拥有的最好的开发人员文档。
  • 更快地编写健壮的代码。
    • 无测试的写代码会导致长时间的调试会话。一个模块中的 bug 的结果可能表现在软件中的一个完全不同的部分。

什么样的测试

有几种测试可以在任何软件上进行。主要有验收测试(或功能测试)和单元测试,这些是大多数人在讨论软件测试话题时会想到的测试。

  • 验收测试。
    • 验收测试(acceptance tests)专注于一个功能,并像黑盒一样处理软件。它只是确保软件真的做了它应该做的,使用与用户相同的媒体并控制输出。这些测试通常是在开发周期中写出来的,以验证应用程序是否满足需求。它们通常作为软件的检查清单运行。通常,这些测试不是通过 TDD 完成的,不过,它们使用 TDD 原则。
  • 单元测试。
    • 单元测试(unit tests)是完全适合测试驱动开发的底层测试。顾名思义,它们专注于测试软件单元。软件单元可以被理解为应用程序代码的最小可测试部分。
  • 功能测试。
    • 功能测试(functional tests)专注于整个特性和功能,而不是小代码单元。它们的目的类似于验收测试。主要区别是功能测试不一定需要使用与用户相同的接口。
  • 集成测试。
    • 集成测试(integration tests)比单元测试代表更高的测试级别。它们测试代码的绝大部分,并专注于许多应用程序层或者组件相互交互的情况。集成测试的形式和范围因项目的架构和复杂性而异。
  • 负载和性能测试。
    • 负载测试和性能测试(Load and performance testing)提供了关于代码效率而不是其正确性的客观信息。负载测试和性能测试的术语可以互换使用,但实际上第一个是指性能的有限方面。负载测试的重点是测量代码在某些人为需求(负载)下的行为。这是一种非常流行的测试 Web 应用程序的方式,其中负载被理解为来自真实用户或程序化客户端的 Web 流量。
  • 代码质量测试。
    • 代码质量没有一个明确说明代码的好坏的衡量标准。不幸的是,代码质量的抽象概念不能用数字的形式来衡量和表示。但是,我们可以测量已知的与代码质量高度相关的软件的各种指标。

达式 Python 标准测试工具

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

  • unittest:这是标准库也是最常见的 Python 单元测试框架,它基于 Java 的 JUnit 框架,最初由 Steve Purcell 编写。
  • doctest:这是一个有读写能力的编程测试工具,它带有交互式使用示例。

unittest

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

该模块是为编写单元测试而创建的,但是只要测试使用用户接口,验收测试也可以用它来编写。

使用 unittest 为一个模块编写一个简单的单元测试,这是通过继承 TestCase 类并且使用 test 前缀来编写方法来完成的。

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

对整个应用程序运行测试的前提是你拥有一个脚本,它可以在所有测试模块中构建测试活动(test campaign)。unittest 提供了一个 TestSuite 类,可以聚合测试并将它们作为测试活动运行,只要它们都是 TestCase 或 TestSuite 的实例。

doctest

doctest 是一个模块,它通过从 docstrings 或文本文件中提取片段以交互式提示会话的形式,重放它们以检查示例输出是否与实际输出相同。

使用doctest有很多优点。

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

做测试

unittest 陷阱

unittest 模块的缺点:

  • 框架有些臃肿,难以使用,原因如下。
    • 必须在 TestCase 的子类中编写所有的测试。
    • 必须在方法名前加上 test。
    • 鼓励使用 TestCase 中提供的断言方法,而不是单纯的断言语句,而现有的方法可能不能覆盖所有的用例。
  • 框架很难扩展,因为它需要大量的子类化其基类或者技巧,如装饰器。
  • 测试固件有时很难组织,因为 setUp 和 tearDown 被绑定到 TestCase 级别,虽然它们在每次测试只运行一次。
  • 在 Python 软件上运行测试活动并不容易。默认测试运行器(python -m unittest)确实提供了一些测试发现功能,但不提供足够的过滤功能。

需要一种更轻量级的方法来编写测试,而不需要一个看起来太像其 Java 兄弟,JUnit 的刻板的框架。由于 Python 不需要使用 100% 的基于类的环境,因此最好提供一个不基于子类化的更 Python 化的测试框架。

常见的方法如下。

  • 提供一种简单的方法来标记任何作为测试的函数或类。
  • 通过插件系统扩展框架。
  • 为所有测试级别提供完整的测试固件环境。整个活动,在测试级别上以模块进行分组。
  • 为基于测试发现的测试运行器提供具有丰富的选项。

unittest 的替代品

两个测试实用程序和框架列表特别受欢迎:

  • nose:http://nose.readthedocs.org。
  • py.test:http://pytest.org。

nose

nose 主要是一个具有强大的发现功能的测试运行器。它有丰富的选项,允许在 Python 应用程序中运行所有类型的测试活动。

nose 通过递归地浏览当前目录进行测试发现并且自己构建一个测试套件。

nose 提供了类似于 TestCase 方法的断言函数(assertionfunctions)。

nose 支持 3 个级别的测试固件。

  • 包级别:可以把 setup 和 teardown 函数添加到 __init__.py 模块中,这是一个包含所有测试模块的测试包。
  • 模块级:测试模块可以有自己的 setup 和 teardown 函数。
  • 测试级别:通过使用 with_setup 装饰器,使固件函数可调用。

nose 可以与 setuptools 平滑地集成,所以可以使用 test 命令进行测试(python setup.py test)。此集成是通过向 setup.py 脚本中添加 test_suite 元数据来完成的。nose 也使用 setuptool 的入口点机制为开发人员写 nose 插件。

py.test

py.test 非常类似于 nose。

一个新的 py.test 命令可以在命令提示符中使用,可以完全像 nosetests 那样使用它。该工具使用类似的模式匹配和测试发现算法捕获要运行的测试。这种模式比 nose 使用的模式更严格并且只会捕获:

  • 以 test 开头的文件中的以 test 开头的类;
  • 以 test 开头的文件中的以 test 开始的函数。

py.test 的优点如下。

  • 能够轻松禁用某些测试类。
  • 一个灵活和新颖的处理测试固件的机制。
  • 在多台计算机之间分发测试的能力。

py.test 支持两种机制来处理固件。第一个,在 xUnit 框架之后建模,类似于 nose。每个函数将获取当前模块,类或方法作为参数。

使用 py.test 编写测试固件的另一种机制是建立在依赖注入的概念上的,允许以更加模块化和可扩展的方式维护测试状态。非xUnit风格的固件(setup / teardown 过程)总是具有唯一的名称,并且需要通过在类中的测试函数,方法和模块中的声明它们的使用来显式激活。

最简单的固件实现形式是采用 pytest.fixture() 装饰器声明的命名函数。要标记测试中使用的固件,它需要声明为一个函数或方法参数。

py.test 提供了一个简单的机制,可以在某些条件下禁用一些测试。这被称为跳过(skipping),并且 pytest 包提供了 .skipif 装饰器支持这种机制。如果在某些条件下需要跳过单个测试函数或整个测试类装饰器,则需要使用此装饰器并且提供的一些用于验证是否满足预期条件的值来定义。

py.test 的一个有趣的特性是它能够跨多台计算机分发测试。只要计算机可以通过 SSH 访问,py.test 就能够驱动每台计算机,发送测试并执行。此功能依赖于网络。

测试覆盖率

代码覆盖率(code coverage)是一个非常有用的度量标准,它提供了有关项目代码的测试的客观信息。它仅仅是在所有测试执行期间执行多少和哪些代码行的测量。

最流行的代码覆盖工具被称为 simply coverage,并在 PyPI 上免费提供。

使用包括两个步骤。第一步是在 shell 中运行 coverage run 命令,并将脚本/程序的路径作为参数运行所有测试。下一步是从 .coverage 文件中的生成一个可读的代码覆盖率报告。

仿真与模拟

写单元测试的前提是要隔离被测试的代码单元。测试通常将一些数据传入函数或方法,并验证其返回值且/或其执行的副作用,这主要是为了确保测试。

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

创建一个仿真

通过发现测试代码与外部部件一起使用所需的最小交互集,可以创建测试中的仿真行为。

仿真类可以随着新的测试的演变来提供更复杂的行为。但它应该尽可能短小和简单。相同的原则可以用于更复杂的输出,通过记录它们作为仿真 API 返回。

使用模拟

模拟对象(mock objects)是可用于隔离所测试代码的通用仿真对象。它们自动化对象的输入和输出的构建过程。

模拟对象或方法的 return_value 参数允许你定义调用返回的值。当使用模拟对象时,每次代码调用某个属性时,它都会为该属性即时创建一个新的模拟对象。

测试环境与依赖兼容性

通过在应用程序级(虚拟环境)和系统级(系统虚拟化)上隔离执行环境,你可以确保测试可以在可重复的条件下运行。这样,你可以避免受因破坏依赖关系导致的罕见且模糊的问题。

正确隔离测试环境的最佳方法是使用支持系统虚拟化的持续集成系统。对于开源项目有很好的免费解决方案,如 Travis CI(Linux 和 OS X)或 AppVeyor(Windows)。

文档驱动开发

与其他语言相比,Python 中的 doctests 是一个真正的优势。通过 doctests 而不是常规的单元测试来构建软件称为文档驱动开发(Document-Driven Development, DDD)。

在 DDD 中,可以通过构建一个代码的来历来编写 doctests,来历主要说明代码如何工作以及何时应该使用代码。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值