Python doctest 模块 | 文档和测试代码

2024 年 8 月 29 日 | 阅读 16 分钟

在本教程中,我们将学习 doctest 模块。它是一个测试框架,可以帮助我们同时进行代码的文档化和测试。该模块允许我们为代码编写文档和测试,这对于编码至关重要。默认情况下,我们可以使用 docstring 来编写类或函数的描述,以更好地理解代码。

我们不需要安装任何第三方库来使用 doctest 模块。但是,应该具备 Python 的基础知识。

文档化示例

代码文档化是最佳实践,资深开发人员建议经常遵循这一实践。可以说,编码及其文档化同等重要。Python 提供了多种方法来为项目、应用程序、模块或脚本编写文档。大型项目通常需要外部文档,而较小的项目可以使用描述性名称、注释和文档字符串(docstrings)来进行文档化。

遵循标准结构并编写文档字符串至关重要。Python 文档中提供了编写文档字符串的原则,还有一些第三方工具和包可以帮助强制执行这些原则。numpydoc 风格是一种广受欢迎的文档字符串格式,用于记录科学 Python 项目。它基于 reStructuredText,并提供了一种一致的方法来清晰简洁地记录函数、类和方法。如果您使用标准的文档字符串格式,其他开发人员可能会更容易理解如何使用和与您的代码交互。

让我们看一个示例,展示如何在程序中有效使用注释

在此示例中,注释用于描述每个代码组件。函数本身之前有一个文档字符串,描述了函数的目的、它接受的参数以及预期的输出。在初始化序列的前两个值之后,函数的代码使用一个 while 循环来生成序列的其余部分,直到达到最大数 n。然后函数以列表形式返回该序列。

我们使用输入值 100 来执行斐波那契算法,并报告结果。使用注释来解释代码,可以使其他开发人员更容易理解代码的功能及其工作原理。

注释的缺点

  • 注释可能会过时: 如果代码被更改,但注释没有更新,那么注释可能无法再正确地反映代码的功能。这可能会对依赖注释来理解代码的其他开发人员造成误导和困惑。
  • 注释可能冗余: 有时,注释只是重复代码本身已经清楚明了的信息。这可能会使代码变得杂乱,更难阅读。
  • 注释可能不清晰或含糊不清: 注释可能不精确或措辞不当,这可能导致混淆和误解。这可能导致代码缺陷和错误。
  • 注释可能被忽略: 如果注释过多或过长,其他开发人员可能会忽略它们。这可能导致重要信息被错过。
  • 注释可能成为拐杖;如果使用过于频繁,其他开发人员可能不会努力去彻底理解代码。这可能导致缺乏理解和不良的编码技巧。

尽管注释有助于澄清代码并使其更易于理解,但谨慎使用并确保它们是正确、清晰和必要的,这一点至关重要。

文档字符串是 Python 中一个非常有用的功能,可以帮助我们在编码时记录我们的代码。而且它比注释有一个优势,因为解释器不会忽略它们。

文档字符串是代码的活动部分;我们可以在运行时访问它。Python 在我们的包、模块、类、方法和函数上提供了 __doc__ 特殊属性。

Python 允许在包、模块、类、方法和函数中包含文档字符串。为了编写有效的文档字符串,建议遵循 PEP 257 中概述的约定和指南。

Doctest 模块简介

开发人员可以使用 Python 的 doctest 模块提供的内置测试结构,创建测试并检查嵌入到文档字符串中的代码示例。它提供了一种实用的方法来同时记录和测试代码,确保文档字符串中包含的代码示例能够产生预期的结果。

doctest 模块的主要目标是鼓励开发人员在他们的文档字符串中提供可执行的代码示例,以推进文档化的最佳实践。这些程序示例展示了如何使用特定的函数、类或模块,同时自动确认其准确性。它们既充当文档又充当测试用例。

doctest 包通过查找 >>> 提示符来在文档字符串中查找交互式 Python 会话。它在这些会话中运行代码,并将结果与文档字符串中声明的预期结果进行比较。如果结果一致,则测试被视为成功;否则,将记录一个失败。

使用 doctest 的一个好处是测试被集成到文档中,这使得保持内容正确和最新变得更容易。为了检查文档字符串中的代码示例在代码修改后是否仍然产生正确的结果,可以运行 doctest。这样做可以保持代码的可靠性并捕捉到可能的衰退。

由于 doctest 模块是 Python 的一个内置组件,它没有外部依赖。它适用于测试中小型代码库,并且轻量级且简单。对于更大、更复杂的程序,其他测试框架(如 unittest 或 pytest)可能更合适。

在 Python 中编写 doctest 测试

到目前为止,我们已经了解了 doctest。现在,我们将学习如何检查函数、方法和其他可调用对象的返回值。我们还将学习如何为代码创建测试用例。最常见的测试用例是检查函数、方法和其他可调用对象的返回值。在下面的示例中,我们有一个函数 multiply(a, b),它接受两个参数并返回两个值的乘积。

您可以使用 doctest 采取以下措施来测试此方法

  • 导入 doctest 模块。
  • 要运行测试,请调用 testmod() 函数
  • testmod() 函数会自动查找并运行包含在该方法文档字符串中的测试。它在将输出与预期结果比较后报告故障或错误。
  • 如果您运行此示例中的代码,doctest 模块将运行包含在 multiply 函数文档字符串中的代码示例。如果所有测试都通过,将不会显示任何输出。但是,如果任何测试失败,doctest 将引发异常并提供有关失败测试的信息。
  • 可以使用 Python 交互式 shell 运行代码并查看测试结果,或者您可以运行整个脚本。
  • 这种 doctest 方法保证了文档字符串中的代码示例保持有效并产生预期的结果。它有助于维护正确的文档,并作为代码的测试工具。

输出

TestResults(failed=0, attempted=3)

这个结果表明三个测试都成功了。它表示没有失败或问题,并且文档字符串中的代码示例产生了预期的结果。

如果任何测试失败,输出将包含有关失败测试、预期输出和实际结果的信息。

更多示例以更好地理解

示例 1:使用多个测试用例测试函数

输出

Trying:
    is_even(4)
Expecting:
    True
ok
Trying:
    is_even(7)
Expecting:
    False
ok
Trying:
    is_even(0)
Expecting:
    True
ok
1 items had no tests:
    __main__
1 items passed all tests:
   3 tests in __main__.is_even
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

说明

  • is_even() 函数接受一个数字作为输入,并检查它是否为偶数。
  • if __name__ == "__main__": 块确保只有在直接执行脚本时才运行 doctest。
  • doctest 验证了三个测试用例
  • is_even(4) 应返回 True,因为 4 是偶数。
  • is_even(7) 应返回 False,因为 7 不是偶数。
  • is_even(0) 应返回 True,因为 0 被认为是偶数。
  • 输出显示所有三个测试都失败了。is_even() 函数返回了预期的结果,但测试用例被标记为失败。这可能是由于预期输出和实际输出不匹配,或者是函数实现不正确。

示例 2:测试具有复杂输出的函数

输出

Trying:
    reverse_list([1, 2, 3, 4])
Expecting:
    [4, 3, 2, 1]
ok
Trying:
    reverse_list(['a', 'b', 'c'])
Expecting:
    ['c', 'b', 'a']
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.reverse_list
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

说明

  • 该程序定义了一个函数 reverse_list(),它反转给定的列表。
  • 该函数的文档字符串包含两个带有预期结果的测试用例,使用了 >>> 符号。
    • reverse_list() 函数接受一个列表作为输入,并返回该列表的反转版本。
    • doctest 检查两个测试用例
    • reverse_list([1, 2, 3, 4]) 应返回 [4, 3, 2, 1],因为列表被反转了。
    • reverse_list(['a', 'b', 'c']) 应返回 ['c', 'b', 'a'],因为列表被反转了。

示例 3:测试带有异常的函数

输出

Trying:
    divide(10, 2)
Expecting:
    5.0
ok
Trying:
    divide(8, 0)
Expecting:
    Traceback (most recent call last):
        ...
    ZeroDivisionError: division by zero
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.divide
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

说明

  • 该程序定义了一个函数 divide(),用于执行两个数的除法。
  • 第二个测试用例期望在除以零时引发 ZeroDivisionError。
  • divide() 函数执行两个数的除法。
  • doctest 验证了两个测试用例
  • divide(10, 2) 应返回 5.0,因为 10 除以 2 是 5.0。
  • divide(8, 0) 应引发 ZeroDivisionError,因为不能除以零。
  • 第一个测试用例成功通过,因为预期结果与实际结果匹配。然而,第二个测试用例失败了,因为函数没有按预期引发 ZeroDivisionError。

示例 4:在测试中忽略输出

输出

Hello, World!
**********************************************************************
1 items had no tests:
    __main__
**********************************************************************
1 items passed all tests:
   1 tests in __main__.print_message
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

说明

  • 该程序定义了一个函数 print_message(),用于向控制台打印一条消息。
  • 该函数的文档字符串包含一个没有预期结果的单一测试用例,因为预期输出是副作用(打印到控制台)。
  • print_message() 函数成功执行,没有任何异常或失败。由于预期输出是副作用(打印到控制台),如果函数执行没有错误,doctest 将认为测试通过。

文档字符串的局限性

第一个局限性

它的第一个缺点是 doctest 模块在处理需要用户交互或具有不确定性行为的复杂测试场景时能力较差。

Doctest 主要测试文档字符串中包含的简短代码片段或示例脚本。主要的比较是在代码的输出和文档字符串中描述的预期输出之间。但是,它不处理预期结果依赖于用户输入或测试环境中无法控制的外部变量的情况。

让我们看一个例子

输出

Failed example:
    greet_user()
Expected:
    Please enter your name: John
    Hello, John! Good to see you.
Got:
    Please enter your name: Hello, Yogendra ! Good to see you.
1 items had no tests:
    __main__
**********************************************************************
1 items had failures:
   1 of   1 in __main__.greet_user
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

在这种情况下,greet_user() 方法会询问用户的姓名,然后打印一条独特的欢迎消息。文档字符串中的预期输出包括用户输入请求和相关的欢迎消息。

然而,doctest 不支持记录用户输入;因此,当此代码与 doctest 一起使用时会失败。测试用例期望用户的姓名和消息“请输入您的姓名:”。但是,doctest 无法模拟或响应用户输入。

运行 doctest 时,测试会挂起,因为它会显示提示符但会无休止地等待人工输入。这个限制是由于 doctest 没有提供在测试运行时模拟或记录用户交互的方法。

为了妥善管理这些场景并处理需要用户交互或不确定性行为的复杂测试用例,有时需要使用更强大的测试框架,如 unittest 或 pytest,它们包含诸如测试夹具(test fixtures)、模拟(mocking)和用户输入模拟等高级功能。

局限性 2:测试私有或内部函数

由于其可见性受限,doctest 无法测试私有或内部函数。在 Python 中,通常在 предназначен于模块内部使用的函数或方法前加上下划线(_),以表示它们不属于公共接口的一部分。这些操作通常被视为实现细节,不应在模块外部直接访问。

Doctest 主要使用模块或脚本的公共接口。因此,它期望被测试的函数或方法可以从其他程序中访问。私有或内部函数很难用 doctest 进行验证,因为它们不应该在模块外部立即被访问。

在这个程序中,我们有一个模块,其中包含公共函数 public_function() 和私有函数 _internal_function()。这两个函数在其文档字符串中都有相应的 doctests。

当使用 doctest.testmod() 函数运行 doctests 时,只有 public_function() 会被测试并生成测试结果。因为 _internal_function() 不属于公共接口,无法被其他代码访问,所以它不会被测试。

使用 doctest 测试内部或私有函数的一种可能方法是使这些方法可以从模块外部访问。然而,这样做违背了最初的设计和封装原则。或者,您可以使用其他测试框架,如 unittest 或 pytest,它们通过在测试用例中直接导入和访问内部或私密方法,在测试这些方法方面提供了更大的自由度。

输出

Trying:
    public_function()
Expecting:
    'This is a public function.'
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.public_function
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

如您所见,测试输出仅提及 public_function() 的测试,表明它成功通过。_internal_function() 没有包含在测试结果中,因为它无法从外部访问。

这个局限性表明,doctest 专注于测试模块的公共接口,可能不适合直接测试私有或内部函数。可以使用其他测试框架,如 unittest 或 pytest 来测试此类函数,这些框架允许对测试私有或内部组件进行更精细的控制。

局限性 3:从外部资源获取资源

doctest 在处理需要外部资源和依赖项的测试时的局限性,指的是测试依赖于外部元素(如文件、数据库、网络连接或 doctest 环境无法控制的其他依赖项)的代码的困难。

Doctest 主要评估文档字符串中包含的代码示例或片段,强调代码在隔离环境中的行为。它不包括处理外部资源或依赖项的内置技术,而这些技术对于测试特定功能可能是必需的。

一个文件名作为参数传递给 read_file() 方法,该方法随后使用 open() 函数读取文件的内容。文档字符串中的预期输出是基于文件 "sample.txt" 存在并且包含文本 "This is the content of the sample file" 的假设。

请按照以下说明运行程序并查看结果

  • 在与 Python 脚本相同的文件夹中,创建一个名为 sample.txt 的文件。
  • 将短语“This is the content of the sample file.”添加到 sample.txt 文件中。
  • 保存文件。

输出

Trying:
    read_file('sample.txt')
Expecting:
    'This is the content of the sample file.'
**********************************************************************
File "c:\Users\HP\Documents\VS Code\Python\jtp.py", line 5, in __main__.read_file
Failed example:
    read_file('sample.txt')
Exception raised:
    Traceback (most recent call last):
      File "C:\Users\HP\AppData\Local\Programs\Python\Python311\Lib\doctest.py", line 1350, in __run
        exec(compile(example.source, filename, "single",
      File "", line 1, in 
        read_file('sample.txt')
      File "c:\Users\HP\Documents\VS Code\Python\jtp.py", line 8, in read_file
        with open(filename, 'r') as file:
             ^^^^^^^^^^^^^^^^^^^
    FileNotFoundError: [Errno 2] No such file or directory: 'sample.txt'
1 items had no tests:
    __main__
**********************************************************************
1 items had failures:
   1 of   1 in __main__.read_file
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

输出表明测试失败,因为找不到 sample.txt 文件。doctest 中的 read_file('sample.txt') 调用引发了 FileNotFoundError 异常,因为文件不存在。此失败在输出中报告, साथ ही traceback 信息显示了异常发生的具体代码行(read_file() 函数中的第 8 行)。测试结果摘要指出,测试中有 1 个失败,并且整个测试标记为“测试失败,有 1 个失败”。

根据错误消息,此代码无法使用 doctest 执行,因为测试无法处理外部资源,即文件“sample.txt”。Doctest 无法在测试期间模拟或仿真文件的存在和内容。

由于文件在测试环境中不可用或无法访问,测试将因 FileNotFoundError 而失败。由于 doctest 缺乏用于管理外部资源或依赖项的集成功能,因此存在此限制。

局限性 5:无法有效处理多种输入变体

doctest 关于参数化测试或具有多种输入变体的测试用例的限制,指的是有效设计和管理包含多个输入值或变体的测试的困难。Doctest 没有内置工具来参数化测试或管理多种输入变体,因为它主要设计用于评估文档字符串中的单个代码示例。

在此示例中,multiply_numbers() 方法将两个数字相乘并返回结果。文档字符串中包含了三个具有不同输入和预期结果的文档。

当使用 doctest 执行此代码时,会执行 doctests,并将函数的实际输出与预期输出进行比较。然而,为了妥善管理参数化测试或多种输入变体,doctest 没有提供内置解决方案。

如果您想包含更多测试用例或参数化测试,在文档字符串中构建多个具有不同输入和输出值的 doctests 会变得困难和重复。测试用例或输入变化越多,这个限制就越明显。

其他测试框架,如 unittest 或 pytest,提供了更具适应性的方法来处理参数化测试,以解决此限制。这些框架允许您使用装饰器构建测试用例,以编程方式生成测试数据,或利用其他数据源,从而提供一种全面且有组织的方法来管理具有多种输入变化的测试。

在处理更复杂的参数化测试或需要多种输入变化的测试用例时,doctest 对于测试变化较少的简单场景可能仍然适用。

结论

总之,Python 的 doctest 模块提供了一种简单轻量的方法,可以将测试插入到模块和函数的文档字符串中。它鼓励通过嵌入式测试用例来描述代码并进行测试,以确保其行为符合预期。当预期结果可以在文档字符串中陈述时,Doctest 特别有用。

然而,了解 doctest 的局限性至关重要。对于处理更复杂的测试需求,例如具有多个输入变体的参数化测试或依赖于外部资源的测试,它可能不是最佳选择。在类似情况下,其他测试框架,如 unittestpytest,提供了更复杂的功能和灵活性。

在选择使用 doctest 还是其他测试框架时,应考虑测试需求的复杂性和范围。对于与文档集成的简短、简单的测试,Doctest 可能很有用,但对于健壮、深入的测试,其他框架可能更合适。

总的来说,doctest 模块对 Python 的测试环境有所贡献,它提供了一种简单且集成的方法来测试文档字符串中包含的代码示例。通过了解其优缺点,开发人员可以在其测试策略中有效地使用 doctest,并为特定情况选择最佳的测试框架。