如何在Python中编写文档测试

来自菜鸟教程
跳转至:导航、​搜索

介绍

文档和测试是每个生产性软件开发过程的核心组成部分。 确保代码被彻底记录和测试不仅可以确保程序按预期运行,而且还支持跨程序员的协作以及用户采用。 在最终编写代码之前,首先编写文档然后进行测试可以很好地为程序员服务。 遵循这样的过程将确保编码的功能(例如)经过深思熟虑并解决可能的边缘情况。

Python 的标准库配备了一个名为 doctest 的测试框架模块。 doctest 模块 以编程方式在 Python 代码中搜索注释中的文本片段,这些文本看起来像交互式 Python 会话。 然后,模块执行这些会话以确认 doctest 引用的代码按预期运行。

此外,doctest 为我们的代码生成文档,提供输入输出示例。 正如 Python 标准库 文档解释的那样,根据您编写 doctest 的方式,这可能更接近于“'literate testing'或'executable documentation'”。

先决条件

您应该在您的计算机或服务器上安装 Python 3 并设置编程环境。 如果您没有设置编程环境,您可以参考本地编程环境或服务器上的编程环境适合您的操作的安装和设置指南系统(Ubuntu、CentOS、Debian 等)

文档测试结构

Python doctest 就像注释一样编写,在 doctest 的顶部和底部有一行三个引号 - """

有时,文档测试是用函数示例和预期输出编写的,但最好还包括对函数打算做什么的注释。 包含注释将确保作为程序员的你已经明确了你的目标,并且未来阅读代码的人会很好地理解它。 请记住,阅读代码的未来程序员很可能就是您。

Info: 要跟随本教程中的示例代码,请通过运行 python3 命令在本地系统上打开 Python 交互式 shell。 然后,您可以通过在 >>> 提示符后添加示例来复制、粘贴或编辑示例。


以下是诸如 add(a, b) 之类将两个数字相加的函数的 doctest 的数学示例:

"""
Given two integers, return the sum.

>>> add(2, 3)
5
"""

在这个例子中,我们有一行解释,还有一个 add() 函数的例子,其中有两个整数作为输入值。 如果将来您希望函数能够添加两个以上的整数,则需要修改 doctest 以匹配函数的输入。

到目前为止,这个 doctest 对人类来说是非常易读的。 您可以通过包含机器可读参数和返回描述来进一步迭代此文档字符串,以解释进出函数的每个变量。

在这里,我们将为传递给函数的两个参数和返回的值添加文档字符串。 文档字符串将记录每个值的 数据类型 — 参数 a、参数 b 和返回值 — 在这种情况下,它们都是整数。

"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>>> add(2, 3)
5
"""

此 doctest 现在已准备好合并到一个函数中并进行测试。

将 Doctest 合并到函数中

Doctests 位于 def 语句之后和函数代码之前的函数中。 由于这遵循函数的初始定义,因此它将按照 Python 的约定进行缩进。

这个简短的函数指示如何合并 doctest。

def add(a, b):
    """
    Given two integers, return the sum.

    :param a: int
    :param b: int
    :return: int

    >>> add(2, 3)
    5
    """
    return a + b

在我们的简短示例中,我们的程序中只有一个函数,所以现在我们必须导入 doctest 模块并有一个调用语句来运行 doctest。

我们将在函数之前和之后添加以下行:

import doctest 
...
doctest.testmod()

此时,让我们在 Python shell 上对其进行测试,而不是现在将其保存到程序文件中。 您可以使用 python3 命令(或 python 如果您使用虚拟 shell)在您选择的命令行终端(包括 IDE 终端)上访问 Python 3 shell。

python3

如果你走这条路线,一旦你按下 ENTER,你将收到类似于以下的输出:

OutputType "help", "copyright", "credits" or "license" for more information.
>>> 

您将能够在 >>> 提示符后开始输入代码。

我们完整的示例代码,包括带有 doctest、docstrings 和调用 doctest 的 add() 函数。 您可以将其粘贴到您的 Python 解释器中进行试用:

import doctest


def add(a, b):
    """
    Given two integers, return the sum.

    :param a: int
    :param b: int
    :return: int

    >>> add(2, 3)
    5
    """
    return a + b

doctest.testmod()

运行代码后,您将收到以下输出:

OutputTestResults(failed=0, attempted=1)

这意味着我们的程序按预期运行!

如果您修改上面的程序,将 return a + b 行改为 return a * b,这会修改函数以将整数相乘并返回它们的乘积,您将收到失败通知:

Output**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
    add(2, 3)
Expected:
    5
Got:
    6
**********************************************************************
1 items had failures:
   1 of   1 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=1)

从上面的输出中,您可以开始了解 doctest 模块的用处,因为它完全描述了 ab 相乘而不是相加时发生的情况,返回 [ 的乘积X212X] 在示例情况下。

您可能想要尝试多个示例。 让我们尝试一个示例,其中两个变量 ab 都包含 0 的值,然后使用 + 运算符将程序改回加法。

import doctest


def add(a, b):
    """
    Given two integers, return the sum.

    :param a: int
    :param b: int
    :return: int

    >>> add(2, 3)
    5
    >>> add(0, 0)
    0
    """
    return a + b

doctest.testmod()

一旦我们运行它,我们将从 Python 解释器收到以下反馈:

OutputTestResults(failed=0, attempted=2)

在这里,输出表明 doctest 在 add(2, 3)add(0, 0) 这两行上尝试了两个测试,并且都通过了。

如果再次将程序更改为使用 * 运算符而不是 + 运算符进行乘法运算,我们可以了解到在使用 doctest 模块时边缘情况很重要,因为第二个示例add(0, 0) 无论是加法还是乘法,都会返回相同的值。

import doctest


def add(a, b):
    """
    Given two integers, return the sum.

    :param a: int
    :param b: int
    :return: int

    >>> add(2, 3)
    5
    >>> add(0, 0)
    0
    """
    return a * b

doctest.testmod()

返回以下输出:

Output**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
    add(2, 3)
Expected:
    5
Got:
    6
**********************************************************************
1 items had failures:
   1 of   2 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)

当我们修改程序时,只有一个例子失败了,但它和以前一样完整地描述了。 如果我们从 add(0, 0) 示例而不是 add(2, 3) 示例开始,我们可能没有注意到当我们的程序的小组件发生更改时存在失败的机会。

编程文件中的 Doctests

到目前为止,我们已经在 Python 交互终端上使用了一个示例。 现在让我们在一个程序文件中使用它来计算单个单词中元音的数量。

在程序中,我们可以在程序文件底部的 if __name__ == "__main__": 子句中导入和调用 doctest 模块。

我们将在我们的文本编辑器中创建一个新文件 - counting_vowels.py,您可以在命令行中使用 nano,如下所示:

nano counting_vowels.py

我们可以从定义函数 count_vowels 开始,并将 word 的参数传递给函数。

计数元音.py

def count_vowels(word):

在我们编写函数的主体之前,让我们解释一下我们希望函数在我们的 doctest 中做什么。

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

到目前为止一切顺利,我们非常具体。 让我们用参数 word 的数据类型和我们想要返回的数据类型来充实这一点。 在第一种情况下它是一个字符串,在第二种情况下它是一个整数。

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

接下来,我们来找例子。 想一个有元音的单词,然后将它输入到文档字符串中。

让我们为秘鲁的城市选择单词 'Cusco'。 “库斯科”有多少个元音? 在英语中,元音通常被认为是aeiou。 所以这里我们将 uo 算作元音。

我们将把 Cusco 的测试和返回的 2 作为整数添加到我们的程序中。

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

同样,拥有多个示例是个好主意。 让我们再举一个元音更多的例子。 我们将使用 'Manila' 前往菲律宾的城市。

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3
    """

那些文档测试看起来很棒,现在我们可以编写我们的程序了。

我们将首先初始化一个 变量total_vowels 来保存元音计数。 接下来,我们将创建一个 for 循环 来遍历 word string 的字母,然后包含一个 条件语句 来检查每个字母是否是元音。 我们将通过循环增加元音计数,然后将单词中元音的总数返回给 total_values 变量。 我们的程序应该与此类似,但没有 doctest:

def count_vowels(word):
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

如果您需要有关这些主题的更多指导,请查看我们的 How To Code in Python 书籍 或补充的 系列

接下来,我们将在程序底部添加我们的 main 子句并导入并运行 doctest 模块:

if __name__ == "__main__":
    import doctest
    doctest.testmod()

此时,这是我们的程序:

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3
    """
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

我们可以使用 python(或 python3 取决于您的虚拟环境)命令来运行程序:

python counting_vowels.py

如果您的程序与上述相同,则所有测试都应该通过,您将不会收到任何输出。 这意味着测试通过了。 当您出于其他目的运行程序时,此静音功能很有用。 如果您专门运行测试,您可能需要使用 -v 标志,如下所示:

python counting_vowels.py -v

当你这样做时,你应该收到这个输出:

OutputTrying:
    count_vowels('Cusco')
Expecting:
    2
ok
Trying:
    count_vowels('Manila')
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.count_vowels
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

优秀的! 测试通过了。 尽管如此,我们的代码可能还没有针对所有边缘情况进行完全优化。 让我们学习如何使用 doctest 来加强我们的代码。

使用 Doctests 改进代码

在这一点上,我们有一个工作程序。 也许它还不是最好的程序,所以让我们试着找到一个边缘案例。 如果我们添加一个大写元音怎么办?

在 doctest 中添加另一个示例,这次让我们为土耳其的城市尝试 'Istanbul'。 和马尼拉一样,伊斯坦布尔也有三个元音。

这是带有新示例的更新程序:

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3

    >>> count_vowels('Istanbul')
    3
    """
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

让我们再次运行程序。

python counting_vowels.py

我们已经确定了一个边缘案例! 这是我们收到的输出:

Output**********************************************************************
File "counting_vowels.py", line 14, in __main__.count_vowels
Failed example:
    count_vowels('Istanbul')
Expected:
    3
Got:
    2
**********************************************************************
1 items had failures:
   1 of   3 in __main__.count_vowels
***Test Failed*** 1 failures.

上面的输出表明 'Istanbul' 上的测试失败。 我们告诉程序我们希望计算三个元音,但程序只计算了两个。 这里出了什么问题?

在我们的 if letter in 'aeiou': 行中,我们只传递了小写元音。 我们可以将我们的 'aeiou' 字符串修改为 'AEIOUaeiou' 以计算大写和小写元音,或者我们可以做一些更优雅的事情,并将我们存储在 word 中的值转换为用 word.lower() 转为小写。 让我们做后者。

计数元音.py

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3

    >>> count_vowels('Istanbul')
    3
    """
    total_vowels = 0
    for letter in word.lower():
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

现在,当我们运行程序时,所有测试都应该通过。 您可以通过运行带有详细标志的 python counting_vowels.py -v 再次确认。

尽管如此,这可能不是最好的程序,它可能没有考虑所有边缘情况。

如果我们将字符串值 'Sydney'(代表澳大利亚的城市)传递给 word 会怎样? 我们会期待三个元音还是一个? 在英语中,y 有时被认为是元音。 此外,如果您使用值 'Würzburg'(代表德国的城市)会发生什么情况,那么 'ü' 会计算在内吗? 应该是? 你将如何处理其他非英语单词? 您将如何处理使用不同字符编码的单词,例如 UTF-16 或 UTF-32 中可用的单词?

作为一名软件开发人员,您有时需要做出棘手的决定,例如决定在示例程序中哪些字符应计为元音。 有时可能没有正确或错误的答案。 在许多情况下,您不会考虑全部可能性。 因此,doctest 模块是一个很好的工具,可以开始思考可能的边缘情况并获取初步文档,但最终您将需要人类用户测试——很可能是合作者——来构建为所有人服务的健壮程序。

结论

本教程介绍了 doctest 模块,它不仅是一种测试和记录软件的方法,也是一种在开始编程之前思考编程的方法,首先记录它,然后测试它,然后编写代码。

不编写测试不仅会导致错误,还会导致软件故障。 养成在编写代码之前编写测试的习惯可以支持为其他开发人员和最终用户服务的生产性软件。

如果您想了解有关测试和调试的更多信息,请查看我们的 “调试 Python 程序”系列。 我们还有一本关于 如何在 Python 中编码 和另一本关于 Python 机器学习项目 的免费电子书。