如何用 Python 编写 Doctests

介绍

文档和测试是每一个富有成效的软件开发过程的核心组成部分。确保代码得到彻底的文档和测试不仅确保程序按预期运行,而且还支持程序员之间的协作以及用户的采用。

Python 的标准库配备了名为doctest的测试框架模块,该模块(doctest模块)(LINK0)会编程地搜索 Python 代码,以便在评论中找到看起来像交互式 Python 会话的文本。

此外,doctest为我们的代码生成文档,提供输入和输出示例. 根据您如何处理写入测试,这可能更接近文本测试可执行的文档,正如Python标准库(https://docs.python.org/3/library/index.html)的文档所解释的那样。

前提条件

如果您没有设置编程环境,您可以参考本地编程环境的安装和安装指南(https://www.digitalocean.com/community/tutorial_series/how-to-install-and-set-up-a-local-programming-environment-for-python-3)或适用于您的操作系统(Ubuntu, CentOS, Debian 等)的编程环境(https://www.digitalocean.com/community/tutorial_collections/how-to-install-python-3-and-set-up-a-programming-environment)。

DocTest 结构

一个 Python 文本测试被写成作为一个评论,在文本测试的顶部和底部有一系列的三个引文标记 - `"" " 。

有时, doctests 是用函数和预期输出的示例写的,但也可能更有可能包括有关该函数的目的的评论。 包括评论将确保你作为程序员已经加密了你的目标,而未来的阅读代码的人会理解它。

<$>[info] 信息: 要跟进本教程中的示例代码,请在本地系统上运行python3命令,打开Python交互壳。

以下是对一个函数的数学示例,例如‘add(a, b)’ 将两个数字加上在一起:

1"""
2Given two integers, return the sum.
3
4>>> add(2, 3)
55
6"""

在本示例中,我们有一个解释行,一个例子是add()函数与输入值的两个整数. 如果在未来,您希望该函数能够添加两个以上的整数,则需要修改 doctest 以匹配函数的输入。

到目前为止,这个 doctest 对于人类来说是非常可读的,您可以通过包括机器可读参数和返回描述来进一步重复这个 docstring 来解释每个变量进入和离开函数。

在这里,我们将为传递到函数的两个参数添加 docstrings 和返回的值. docstring 会记录每个值的 数据类型 - 参数 a、参数 b 和返回的值 - 在这种情况下,它们都是整数。

 1"""
 2Given two integers, return the sum.
 3
 4:param a: int
 5:param b: int
 6:return: int
 7
 8>>> add(2, 3)
 95
10"""

此 doctest 现在已经准备好将其纳入一个功能并进行测试。

将 Doctest 集成到一个函数中

Doctests 位於函數後的「def」聲明和函數的代碼之前. 因為這跟隨了函數的初始定義,它將根據Python的公約編入。

此短函数表明如何嵌入 doctest。

 1def add(a, b):
 2    """
 3    Given two integers, return the sum.
 4
 5    :param a: int
 6    :param b: int
 7    :return: int
 8
 9    >>> add(2, 3)
10    5
11    """
12    return a + b

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

我们将在我们的职能之前和之后添加以下行:

1import doctest 
2...
3doctest.testmod()

在此时刻,让我们在Python壳上测试它,而不是现在将其保存到程序文件中,您可以使用python3命令(如果您正在使用虚拟壳)访问您所选择的命令行终端上的Python 3壳。

1python3

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

1[secondary_label Output]
2Type "help", "copyright", "credits" or "license" for more information.
3>>>

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

我们的完整示例代码,包括一个 doctest、docstrings 和调用 doctest 的 add() 函数,如下所示:您可以将其粘贴到您的 Python 解释器中,以便尝试:

 1import doctest
 2
 3def add(a, b):
 4    """
 5    Given two integers, return the sum.
 6
 7    :param a: int
 8    :param b: int
 9    :return: int
10
11    >>> add(2, 3)
12    5
13    """
14    return a + b
15
16doctest.testmod()

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

1[secondary_label Output]
2TestResults(failed=0, attempted=1)

这意味着我们的计划按照预期进行!

如果您更改上述程序,以便返回 a + b行代替返回 a * b,从而更改函数以倍增整数并返回其产品,您将收到一个错误通知:

 1[secondary_label Output]
 2**********************************************************************
 3File "__main__", line 9, in __main__.add
 4Failed example:
 5    add(2, 3)
 6Expected:
 7    5
 8Got:
 9    6
10**********************************************************************
111 items had failures:
12   1 of 1 in __main__.add
13***Test Failed*** 1 failures.
14TestResults(failed=1, attempted=1)

从上面的输出中,你可以开始理解测试模块有多么有用,因为它完全描述了当ab被倍增而不是添加时发生的事情,在示例中返回了6的产物。

让我们尝试一个例子,其中ab的变量都包含0的值,然后将程序更改为加上+运算器。

 1import doctest
 2
 3def add(a, b):
 4    """
 5    Given two integers, return the sum.
 6
 7    :param a: int
 8    :param b: int
 9    :return: int
10
11    >>> add(2, 3)
12    5
13    >>> add(0, 0)
14    0
15    """
16    return a + b
17
18doctest.testmod()

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

1[secondary_label Output]
2TestResults(failed=0, attempted=2)

在这里,输出表明,试验器尝试了两个测试,在添加(2,3)添加(0,0)的两行,并且两者都通过了。

如果,再次,我们更改程序以使用*运算器来倍增而不是+运算器,我们可以学习到边缘情况在与 doctest 模块工作时很重要,因为第二个例子add(0,0)将返回相同的值,无论是加或倍增。

 1import doctest
 2
 3def add(a, b):
 4    """
 5    Given two integers, return the sum.
 6
 7    :param a: int
 8    :param b: int
 9    :return: int
10
11    >>> add(2, 3)
12    5
13    >>> add(0, 0)
14    0
15    """
16    return a * b
17
18doctest.testmod()

返回以下输出:

 1[secondary_label Output]
 2**********************************************************************
 3File "__main__", line 9, in __main__.add
 4Failed example:
 5    add(2, 3)
 6Expected:
 7    5
 8Got:
 9    6
10**********************************************************************
111 items had failures:
12   1 of 2 in __main__.add
13***Test Failed*** 1 failures.
14TestResults(failed=1, attempted=2)

当我们修改程序时,只有一个例子失败了,但它完全像以前一样描述了。如果我们从添加(0,0)的例子开始,而不是添加(2,3)的例子,我们可能没有注意到当我们程序的小组件发生变化时出现失败的可能性。

编程文件中的Doctests

到目前为止,我们已经使用了 Python 交互式终端的例子,现在让我们在一个编程文件中使用它,该文件将计算单个单词中的语音数目。

在一个程序中,我们可以在我们的编程文件底部的if __name__ ==``__main__条款中导入和调用 doctest 模块。

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

1nano counting_vowels.py

我們可以開始定義我們的函數「count_vowels」並將「word」的參數傳遞給函數。

1[label counting_vowels.py]
2def count_vowels(word):

在我们写函数的身体之前,让我们解释我们希望函数在我们的测试中做什么。

1[label counting_vowels.py]
2def count_vowels(word):
3    """
4    Given a single word, return the total number of vowels in that single word.

到目前为止,我们已经非常具体了,让我们用参数的数据类型和我们想要返回的数据类型来形容。

1[label counting_vowels.py]
2def count_vowels(word):
3    """
4    Given a single word, return the total number of vowels in that single word.
5
6    :param word: str
7    :return: int

接下来,让我们找个例子:想想一个单一的单词,它有语音,然后在字符串中输入它。

让我们为秘鲁的城市选择Cusco这个词。在Cusco中有多少个字母?在英语中,字母通常被认为是a,e,i,ou

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

 1[label counting_vowels.py]
 2def count_vowels(word):
 3    """
 4    Given a single word, return the total number of vowels in that single word.
 5
 6    :param word: str
 7    :return: int
 8
 9    >>> count_vowels('Cusco')
10    2

再一次,有不止一个例子是一个好主意,让我们有另一个例子,有更多的歌词,我们将带着马尼拉去菲律宾的城市。

 1[label counting_vowels.py]
 2def count_vowels(word):
 3    """
 4    Given a single word, return the total number of vowels in that single word.
 5
 6    :param word: str
 7    :return: int
 8
 9    >>> count_vowels('Cusco')
10    2
11
12    >>> count_vowels('Manila')
13    3
14    """

这些测试看起来很棒,现在我们可以编码我们的程序。

我们将开始初始化一个 变量 - 'total_vowels' 以保持音符数。接下来,我们将创建一个 for loop 重复在字母中的 word string,然后包括一个 条件声明 检查每个字母是否是一个音符。 我们将通过循环增加音符数,然后将单词中的音符总数返回到 total_values 变量。

1def count_vowels(word):
2    total_vowels = 0
3    for letter in word:
4        if letter in 'aeiou':
5            total_vowels += 1
6    return total_vowels

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

接下来,我们将在程序底部添加我们的主要条款,并导入并运行 doctest 模块:

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

此时此刻,这是我们的计划:

 1[label counting_vowels.py]
 2def count_vowels(word):
 3    """
 4    Given a single word, return the total number of vowels in that single word.
 5
 6    :param word: str
 7    :return: int
 8
 9    >>> count_vowels('Cusco')
10    2
11
12    >>> count_vowels('Manila')
13    3
14    """
15    total_vowels = 0
16    for letter in word:
17        if letter in 'aeiou':
18            total_vowels += 1
19    return total_vowels
20
21if __name__ == "__main__":
22    import doctest
23    doctest.testmod()

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

1python counting_vowels.py

如果您的程序与上述相同,所有测试都应该通过,您将不会收到任何输出,这意味着测试已经通过,此默默功能在运行其他用途的程序时非常有用。

1python counting_vowels.py -v

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

 1[secondary_label Output]
 2Trying:
 3    count_vowels('Cusco')
 4Expecting:
 5    2
 6ok
 7Trying:
 8    count_vowels('Manila')
 9Expecting:
10    3
11ok
121 items had no tests:
13    __main__
141 items passed all tests:
15   2 tests in __main__.count_vowels
162 tests in 2 items.
172 passed and 0 failed.
18Test passed.

很棒! 测试已经通过. 然而,我们的代码可能尚未完全优化为所有边缘案例. 让我们学习如何使用 doctests 来加强我们的代码。

使用 Doctests 来改进代码

在这个时候,我们有一个工作程序. 也许它还不是最好的程序,所以让我们试着找到一个边缘案例。

再加上另一個例子,這次讓我們嘗試「伊斯坦布爾」為土耳其的城市,就像馬尼拉一樣,伊斯坦布爾也有三個聲音。

以下是我們更新的計劃與新的例子:

 1[label counting_vowels.py]
 2def count_vowels(word):
 3    """
 4    Given a single word, return the total number of vowels in that single word.
 5
 6    :param word: str
 7    :return: int
 8
 9    >>> count_vowels('Cusco')
10    2
11
12    >>> count_vowels('Manila')
13    3
14
15    >>> count_vowels('Istanbul')
16    3
17    """
18    total_vowels = 0
19    for letter in word:
20        if letter in 'aeiou':
21            total_vowels += 1
22    return total_vowels
23
24if __name__ == "__main__":
25    import doctest
26    doctest.testmod()

让我们再次运行这个程序。

1python counting_vowels.py

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

 1[secondary_label Output]
 2**********************************************************************
 3File "counting_vowels.py", line 14, in __main__.count_vowels
 4Failed example:
 5    count_vowels('Istanbul')
 6Expected:
 7    3
 8Got:
 9    2
10**********************************************************************
111 items had failures:
12   1 of 3 in __main__.count_vowels
13***Test Failed*** 1 failures.

上面的结果表明,伊斯坦布尔的测试是失败的,我们告诉节目,我们预计将计算三个歌词,但相反,节目只计算了两个。

在我们的行中,如果字母是aeiou:我们只通过了较低的字符串,我们可以将我们的aeiou字符串修改为AEIOUaeiou,以计算上和下的字符串,或者我们可以做一些更优雅的事情,并将我们存储在word中的值转换为较低的字符串,使用word.lower()

 1[label counting_vowels.py]
 2def count_vowels(word):
 3    """
 4    Given a single word, return the total number of vowels in that single word.
 5
 6    :param word: str
 7    :return: int
 8
 9    >>> count_vowels('Cusco')
10    2
11
12    >>> count_vowels('Manila')
13    3
14
15    >>> count_vowels('Istanbul')
16    3
17    """
18    total_vowels = 0
19    for letter in word.lower():
20        if letter in 'aeiou':
21            total_vowels += 1
22    return total_vowels
23
24if __name__ == "__main__":
25    import doctest
26    doctest.testmod()

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

然而,这可能不是最好的程序,它可能不会考虑所有的边缘案例。

如果我们将悉尼字符串值转换为澳大利亚城市字符串值,我们是否会期望三个字符串或一个字符串?在英语中,y有时被认为是一个字符串。此外,如果您使用Würzburg字符串值 – 对于德国的城市,ü字符串会计数吗?应该吗?您将如何处理其他非英语单词?您将如何处理使用不同字符编码的单词,例如在 UTF-16 或 UTF-32中的单词)?

作为一个软件开发人员,你有时需要做出困难的决定,例如决定在示例程序中将哪些字符计算为语音。有时可能没有正确或错误的答案。在许多情况下,你不会考虑到所有可能性。

结论

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

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

如果您想了解有关测试和调试的更多信息,请查看我们的调试 Python 程序系列(https://www.digitalocean.com/community/tutorial_series/debugging-python-programs)。

Published At
Categories with 技术
comments powered by Disqus