在本章中,我们将介绍以下配方:
- 使用 docstring 进行测试
- 测试引发异常的函数
- 处理常见的 doctest 问题
- 创建单独的测试模块和包
- 联合单元测试和博士测试
- 测试涉及日期或时间的事物
- 测试涉及随机性的事物
- 嘲笑外部资源
测试是创建工作软件的核心。以下是关于测试重要性的规范声明:
没有自动测试的程序功能根本不存在。
这是肯特·贝克的书《极限编程解释:拥抱变化》。
我们可以区分几种测试:
- 单元测试:适用于软件的独立单元:函数、类或模块。对装置进行隔离测试,以确认其工作正常。
- 集成测试:组合单元以确保它们正确集成。
- 系统测试:测试整个应用程序或相关应用程序系统,以确保聚合的软件组件套件正常工作。这通常用于软件适合使用的总体验收。
- 性能测试:确保机组达到性能目标。在某些情况下,性能测试包括对内存、线程或文件描述符等资源的研究。目标是确保软件适当地使用系统资源。
Python 有两个内置的测试框架。我们可以检查 docstring 以查找包含>>>
提示符的示例。这是doctest
工具。虽然这广泛用于单元测试,但也可以用于简单的集成测试。
另一个测试框架使用由unittest
模块中的定义构建的类。此模块定义了一个TestCase
类。这也是主要为单元测试而设计的,但也可以应用于集成和性能测试。
当然,我们希望结合这些工具。这两个模块都具有允许共存的功能。我们通常会利用unittest
包中的测试加载协议来合并所有测试。
此外,我们可能会使用工具nose2
或py.test
进一步自动化测试发现,并添加额外的功能,如测试用例覆盖率。这些项目通常对特别复杂的应用程序很有帮助。
有时,使用给定的测试用例命名的 WHEN-THEN 样式总结测试是很有帮助的:
- 给定一些初始状态或上下文
- 当请求行为时
- 则被测部件有某种预期结果或状态变化
好的 Python 在每个模块、类、函数和方法中都包含 docstring。许多工具可以从 docstring 中创建有用的、信息丰富的文档。
docstring 的一个重要元素是示例。这个例子变成了一种单元测试用例。一个示例通常适合于给定的 WHEN-THEN 测试模型,因为它显示了一个单元、一个请求和一个响应。
我们如何将示例转化为适当的测试用例?
我们将看一个简单的函数定义和一个简单的类定义。其中每一个都将包括文档字符串,其中包括可以用作正式测试的示例。
这里有一个简单的函数,可以计算两个数的二项式系数。它显示了n事物在k大小组中的组合数量。例如,一副 52 张牌的牌可以分为 5 张牌,计算方法如下:
这定义了一个小 Python 函数,我们可以这样编写:
from math import factorial
def binom(n: int, k: int) -> int:
return factorial(n) // (factorial(k) * factorial(n-k))
此函数执行简单计算并返回一个值。因为它没有内部状态,所以测试起来相对容易。这将是用于显示可用单元测试工具的示例之一。
我们还将看一个简单的类,它有一个懒散的均值和中位数计算。它使用一个内部Counter
对象,该对象可以被询问以确定模式:
from statistics import median
from collections import Counter
class Summary:
def __init__(self):
self.counts = Counter()
def __str__(self):
return "mean = {:.2f}\nmedian = {:d}".format(
self.mean, self.median)
def add(self, value):
self.counts[value] += 1
@property
def mean(self):
s0 = sum(f for v,f in self.counts.items())
s1 = sum(v*f for v,f in self.counts.items())
return s1/s0
@property
def median(self):
return median(self.counts.elements())
add()
方法更改此对象的状态。由于这种状态变化,我们需要提供更复杂的示例来展示Summary
类实例的行为。
我们将展示此配方中的两种变体。第一种是基本上无状态的操作,例如计算binom()
函数。第二个是有状态操作,比如Summary
类。
- 将示例放入文档字符串中。
- 将 doctest 模块作为程序运行。这可以通过以下两种方式之一实现:
-
At the command prompt:
$ python3.5 -m doctest code/ch11_r01.py
如果所有示例都通过,则没有输出。使用
-v
选项会生成详细的输出,总结测试。 -
By including a
__name__ == '__main__'
section. This can import the doctest module and execute thetestmod()
function:if __name__ == '__main__': import doctest doctest.testmod()
如果所有示例都通过,则没有输出。要查看一些输出,请使用
testmod()
函数的verbose=1
参数创建更详细的输出。
-
-
以摘要开始文档字符串:
'''Computes the binomial coefficient. This shows how many combinations of *n* things taken in groups of size *k*.
-
包括参数定义:
:param n: size of the universe :param k: size of each subset
-
包含返回值定义:
:returns: the number of combinations
-
模拟一个在 Python 的
>>>
提示符下使用函数的示例:>>> binom(52, 5) 2598960
-
用适当的引号关闭长文档字符串:
'''
-
Write a class-level docstring with a summary:
'''Computes summary statistics. '''
我们已经留出了空间来填写示例。
-
编写带有摘要的方法级 docstring。以下是
add()
方法:def add(self, value): '''Adds a value to be summarized. :param value: Adds a new value to the collection. ''' self.counts[value] += 1
-
Here's the
mean()
method:@property def mean(self): '''Computes the mean of the collection. :return: mean value as a float ''' s0 = sum(f for v,f in self.counts.items()) s1 = sum(v*f for v,f in self.counts.items()) return s1/s0
median()
方法和任何其他写入的方法都需要类似的字符串。 -
Extend the class-level docstring concrete examples. In this case, we'll write two. The first example shows that the
add()
method has no return value, but changes the state of the object. Themean()
method reveals this state:>>> s = Summary() >>> s.add(8) >>> s.add(9) >>> s.add(9) >>> round(s.mean, 2) 8.67 >>> s.median 9
我们对平均值的结果进行了四舍五入,以避免在所有平台上显示可能不具有完全相同的文本表示形式的长浮点值。当我们运行 doctest 时,通常会得到一个静默响应,因为测试通过了。
第二个示例显示了来自__str__()
方法的多行结果:
>>> print(str(s))
mean = 8.67
median = 9
当某些东西不起作用时会发生什么?想象一下,我们改变了预期的输出,得到了一个错误的答案。运行 doctest 时,我们将看到如下输出:
*********************************************************************
File "__main__", line ?, in __main__.Summary
Failed example:
s.median
Expected:
10
Got:
9
*********************************************************************
1 items had failures:
1 of 6 in __main__.Summary
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=9)
这显示了错误所在的位置。它显示了测试示例中的预期值和实际答案。
doctest
模块包括一个主程序和几个函数,这些函数将扫描 Python 文件中的>>>
示例。我们可以利用模块扫描功能testmod()
来扫描当前模块。我们可以用它来扫描任何导入的模块。
扫描操作查找具有>>>
行特征模式的文本块,后跟显示命令响应的行。
doctest 解析器从提示行和响应文本块创建一个小测试用例对象。有三种常见情况:
- 没有预期的响应文本:当我们为
Summary
类的add()
方法定义测试时,我们看到了这种模式。 - 单行响应文本:
binom()
函数和mean()
方法举例说明了这一点。 - 多行响应:响应由下一个
>>>
提示或空行限定。这在Summary
类的str()
示例中得到了证明。
doctest 模块将执行显示有>>>
提示的每一行代码。它将实际结果与预期结果进行比较。比较是一种非常简单的文本匹配。除非使用特殊注释,否则输出必须与预期完全匹配。
该测试协议的简单性提出了一些软件设计要求。函数和类必须设计为在>>>
提示符下工作。因为作为 docstring 示例的一部分创建非常复杂的对象可能会变得很尴尬,所以设计必须保持足够简单,以便能够以交互方式演示。保持软件足够简单以在>>>
提示符下演示通常是有益的。
结果比较的简单性可能会对正在显示的输出造成一些复杂性。例如,请注意,我们将平均值四舍五入到小数点后两位。这是因为浮点值的显示可能因平台而异。
Python3.5.1(在 Mac OS X 上)显示了8.666666666666666
,而 Python2.6.9(同样在 Mac OS X 上)显示了8.6666666666666661
。16 位十进制数字的值相等。这大约是 48 位数据,这是浮点值的实际限制。
我们将在处理常见 doctest 问题配方中详细讨论精确比较问题。
一个重要的测试考虑是边缘情况。边缘情况通常侧重于设计计算的限制。例如,二项式函数有两条边:
我们可以很容易地将这些添加到示例中,以确保我们的实现是可靠的;这将产生一个如下所示的函数:
def binom(n: int, k: int) -> int:
'''Computes the binomial coefficient.
This shows how many combinations of
*n* things taken in groups of size *k*.
:param n: size of the universe
:param k: size of each subset
:returns: the number of combinations
>>> binom(52, 5)
2598960
>>> binom(52, 0)
1
>>> binom(52, 52)
1
'''
return factorial(n) // (factorial(k) * factorial(n-k))
在某些情况下,我们可能需要测试超出有效值范围的值。这些案例并不适合放入 docstring,因为它们将对应该发生的事情的解释与通常不应该发生的其他事情的解释混为一谈。
我们可以在名为__test__
的全局变量中包含额外的 docstring 测试用例。此变量必须是映射。映射的键是测试用例名称,映射的值是 doctest 示例。这些将需要三重引号字符串。
由于示例不在 docstring 中,因此在使用内置的help()
函数时不会显示这些示例。当使用其他工具从源代码创建文档时,它们也不会出现。
我们可以添加如下内容:
__test__ = {
'GIVEN_binom_WHEN_0_0_THEN_1':
'''
>>> binom(0, 0)
1
''',
}
我们已经用没有缩进的键编写了映射。这些值已缩进四个空格,以便它们从关键帧中突出,更容易识别。
这些测试用例由 doctest 程序找到,并包含在整个测试套件中。我们可以将其用于重要的测试,但作为文档并没有真正的帮助。
- 在产生异常的测试函数和处理常见 doctest 问题配方中,我们将研究另外两种 doctest 技术。这些都很重要,因为异常通常包括回溯,回溯可能包括每次程序运行时都会变化的对象 ID。
好的 Python 在每个模块、类、函数和方法中都包含 docstring。许多工具可以从这些 docstring 创建有用的、信息丰富的文档。
docstring 的一个重要元素是示例。这个例子变成了一种单元测试用例。Doctest 将预期输出与实际输出进行简单的文字匹配。
但是,当一个示例引发异常时,来自 Python 的回溯消息并不总是相同的。它可能包括改变的对象 ID 值或模块行号,这些行号可能根据执行测试的上下文略有不同。当涉及异常时,doctest 的文本匹配规则不合适。
我们如何将异常处理和由此产生的回溯消息转化为适当的测试用例?
我们将看一个简单的函数定义和一个简单的类定义。其中每一个都将包括文档字符串,其中包括可以用作正式测试的示例。
这里有一个简单的函数,可以计算两个数的二项式系数。它显示了k组中n事物的组合数。例如,一副 52 张牌的牌有多少种方式可以分为 5 张牌:
这定义了一个小 Python 函数,我们可以这样编写:
from math import factorial
def binom(n: int, k: int) -> int:
'''
Computes the binomial coefficient.
This shows how many combinations of
*n* things taken in groups of size *k*.
:param n: size of the universe
:param k: size of each subset
:returns: the number of combinations
>>> binom(52, 5)
2598960
'''
return factorial(n) // (factorial(k) * factorial(n-k))
此函数执行简单计算并返回一个值。我们希望在__test__
变量中包含一些额外的测试用例,以显示当给定值超出预期范围时,它会做什么。
-
Create a global
__test__
variable in the module:__test__ = { }
我们已经留出了插入一个或多个测试用例的空间。
-
对于每个测试用例,为示例提供名称和占位符:
__test__ = { 'GIVEN_binom_WHEN_wrong_relationship_THEN_error': ''' example goes here. ''', }
-
Include the invocation with a
doctest
directive comment,IGNORE_EXCEPTION_DETAIL
. This replaces the "example goes here":>>> binom(5, 52) # doctest: +IGNORE_EXCEPTION_DETAIL
该指令以
# doctest:
开头。指令通过+
启用,通过-
禁用。 -
包括实际的回溯消息。这是示例的一部分;它在
>>>
语句之后显示预期的响应:Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/doctest.py", line 1320, in __run compileflags, 1), test.globs) File "<doctest __main__.__test__.GIVEN_binom_WHEN_wrong_relationship_THEN_error[0]>", line 1, in <module> binom(5, 52) File "/Users/slott/Documents/Writing/Python Cookbook/code/ch11_r01.py", line 24, in binom return factorial(n) // (factorial(k) * factorial(n-k)) ValueError: factorial() not defined for negative values
-
以
File...
开头的三行将被忽略。将检查ValueError:
行,以确保测试产生预期的异常。
整体陈述如下:
__test__ = {
'GIVEN_binom_WHEN_wrong_relationship_THEN_error': '''
>>> binom(5, 52) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/doctest.py", line 1320, in __run
compileflags, 1), test.globs)
File "<doctest __main__.__test__.GIVEN_binom_WHEN_wrong_relationship_THEN_error[0]>", line 1, in <module>
binom(5, 52)
File "/Users/slott/Documents/Writing/Python Cookbook/code/ch11_r01.py", line 24, in binom
return factorial(n) // (factorial(k) * factorial(n-k))
ValueError: factorial() not defined for negative values
'''
}
我们现在可以使用如下命令来测试整个模块的功能:
python3.5 -R -m doctest ch11_r01.py
doctest 解析器有几个指令可用于修改测试行为。指令作为特殊注释包含在执行测试操作的代码行中。
我们有两种方法来处理包含异常的测试:
- 我们可以使用
# doctest: +IGNORE_EXCEPTION_DETAIL
并提供完整的回溯错误消息。回溯的详细信息将被忽略,只有最后的异常行与预期值匹配。这使得复制实际错误并将其粘贴到文档中变得非常容易。 - 我们可以使用
# doctest: +ELLIPSIS
并将部分回溯消息替换为...
。这也允许预期的输出省略细节,并将焦点放在最后一行上,该行有实际的错误。
对于第二类异常示例,我们可能会包含如下测试用例:
'GIVEN_binom_WHEN_negative_THEN_exception':
'''
>>> binom(52, -5) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: factorial() not defined for negative values
''',
测试用例使用+ELLIPSIS
指令。错误回溯的细节已用...
替换了不相关的材料。相关材料保持不变,以便实际异常消息与预期异常消息精确匹配。
Doctest 将忽略第一行Traceback...
和最后一行ValueError: ...
之间的所有内容。通常,最后一行是正确执行测试的全部内容。中间文本取决于运行测试的上下文。
还有几个比较指令可以提供给各个测试。
+ELLIPSIS
:这允许通过将细节替换为...
来概括预期结果。+IGNORE_EXCEPTION_DETAIL
:这允许预期值包含完整的回溯消息。大部分回溯将被忽略,并且只检查最后的异常行。+NORMALIZE_WHITESPACE
:在某些情况下,期望值可能会被包装到多行中,以便于阅读。或者,它的间距可能与标准 Python 值略有不同。使用此标志允许在预期值的空白处具有一定的灵活性。+SKIP
:跳过测试。这有时用于为将来的版本设计的测试。在功能完成之前,可包括测试。测试可以保留在未来的开发工作中,但为了按时发布版本,可以跳过测试。+DONT_ACCEPT_TRUE_FOR_1
:这涵盖了 Python 2 中常见的一种特殊情况。在将True
和False
添加到语言之前,使用了1
和0
值。用于将预期结果与实际结果进行比较的 doctest 算法将通过匹配True
和1
来支持此旧方案。可在命令行使用-o DONT_ACCEPT_TRUE_FOR_1
提供此指令。然后,对于所有测试,此更改都是全局真实的。+DONT_ACCEPT_BLANKLINE
:通常情况下,一个示例以空行结尾。如果示例输出包含空行,则预期结果必须使用特殊语法<blankline>
。使用此选项可以显示预期的空行位置,并且示例不会到此空行结束。在极少数情况下,预期输出实际上将包括字符串<blankline>
。本指令确保<blankline>
不用于表示空行,而是代表其本身。这在为 doctest 模块本身编写测试时是有意义的。
在评估testmod()
或testfile()
功能时,这些也可以作为optionsflags
参数提供。
- 有关 doctest 的基础知识,请参见使用 docstrings 进行测试配方
- 其他需要 doctest 指令的特殊情况,请参见处理常见 doctest 问题配方
好的 Python 在每个模块、类、函数和方法中都包含 docstring。许多工具可以从最少完整的 docstring 创建有用的、信息丰富的文档。
docstring 的一个重要元素是示例。这个例子变成了一种单元测试用例。Doctest 将预期输出与实际输出进行简单的文字匹配。但是,有些 Python 对象在每次引用时并不一致。
例如,所有对象哈希值都是随机的。这意味着集合中元素的顺序或字典中键的顺序可能不同。对于创建测试用例示例输出,我们有几种选择:
- 编写能够容忍随机化的测试。通常通过转换为排序结构。
- 为
PYTHONHASHSEED
环境变量规定一个值。 - 要求 Python 使用
-R
选项运行,以完全禁用散列随机化。
除了按键或项目在集合中的位置的简单可变性之外,还有其他一些考虑因素。以下是一些其他问题:
id()
和repr()
函数可能会公开内部对象 ID。无法保证这些值。- 浮点值可能因平台而异。
- 当前日期和时间不能在测试用例中有意义地使用。
- 使用默认种子的随机数很难预测。
- 操作系统资源可能不存在,或者可能未处于正确状态。
我们将通过本配方中的一些 doctest 技术重点讨论前两个问题。我们将在测试涉及日期或时间的事物和测试涉及随机性配方的事物时,看看datetime
和random
。我们将在模仿外部资源配方中了解如何使用外部资源。
Doctest 示例要求与文本完全匹配。我们如何编写适当处理散列随机化或浮点实现细节的 doctest 示例?
在使用 CSV 模块配方的读取分隔文件中,我们了解了csv
模块将如何读取数据,为每行输入创建一个映射。在该配方中,我们看到了一个CSV
文件,其中包含一些从帆船日志中记录的实时数据。这是waypoints.csv
文件。
DictReader
类生成如下所示的行:
{'date': '2012-11-27',
'lat': '32.8321666666667',
'lon': '-79.9338333333333',
'time': '09:15:00'}
这是一个博士级的噩梦,因为散列随机化确保了该字典中键的顺序可能不同。
当我们试图编写涉及词典的 doctest 示例时,我们经常会看到这样的问题:
Failed example:
next(row_iter)
Expected:
{'date': '2012-11-27', 'lat': '32.8321666666667',
'lon': '-79.9338333333333', 'time': '09:15:00'}
Got:
{'lon': '-79.9338333333333', 'time': '09:15:00',
'date': '2012-11-27', 'lat': '32.8321666666667'}
预期行和实际行中的数据明显匹配。但是,字典值的字符串显示并不完全相同。按键的显示顺序不一致。
我们还将介绍一个小型实值函数,以便使用浮点值:
该函数是标准 z 分数的累积概率密度函数。标准化变量后,该变量的 Z 值平均值为零,标准偏差为一。参见第 8 章功能性和反应性编程特性中的创建部分功能配方,了解关于标准化分数的更多信息。
这个函数Φ(n)准确地告诉我们人口中有多少部分低于给定的 z 分数。例如,Φ(0)=0.5:一半人口的 z 分数低于零。
此函数涉及一些相当复杂的处理。单元测试必须反映浮点精度问题。
我们将在一个配方中查看映射(并设置)顺序。我们将分别研究浮点。
-
Import the necessary libraries and define the function:
import csv def raw_reader(data_file): """ Read from a given, open file. :param data_file: Open file, ready to be processed. :returns: iterator over individual rows as dictionaries. Example: """ data_reader = csv.DictReader(data_file) for row in data_reader: yield row
我们已经在文档字符串中包含了示例标题。
-
我们可以用
io
包中StringIO
类的实例替换实际数据文件。可在示例内部使用,以提供固定样本数据:>>> from io import StringIO >>> mock_file = StringIO('''lat,lon,date,time ... 32.8321,-79.9338,2012-11-27,09:15:00 ... ''') >>> row_iter = iter(raw_reader(mock_file))
-
Conceptually, the test case is this. This code will not work properly because the keys will be scrambled. However, it can be refactored easily:
>>> row = next(row_iter) >>> row {'time': '09:15:00', 'lat': '32.8321', etc. }
我们省略了输出的其余部分,因为它在每次运行测试时都会发生变化:
代码必须这样编写,以强制键进入固定顺序:
>>> sorted(row.items()) # doctest: +NORMALIZE_WHITESPACE [('date', '2012-11-27'), ('lat', '32.8321'), ('lon', '-79.9338'), ('time', '09:15:00')]
排序的项目顺序一致。
-
Import the necessary libraries and define the function:
from math import * def phi(n): """ The cumulative distribution function for the standard normal distribution. :param n: number of standard deviations :returns: cumulative fraction of values below n. Examples: """ return (1+erf(n/sqrt(2)))/2
我们在文档字符串中为示例留出了空间。
-
对于每个示例,包括明确使用
round()
:>>> round(phi(0), 3) 0.399 >>> round(phi(-1), 3) 0.242 >>> round(phi(+1), 3) 0.242
浮点值是四舍五入的,因此浮点实现细节的差异不会导致看似不正确的结果。
由于散列随机化,用于字典的散列键是不可预测的。这是一项重要的安全功能,可以抵御隐蔽的拒绝服务攻击。详见http://www.ocert.org/advisories/ocert-2011-003.html 。
我们有两种方法来处理没有定义顺序的字典键:
-
我们可以编写特定于每个键的测试用例:
>>> row['date'] '2012-11-27' >>> row['lat'] '32.8321' >>> row['lon'] '-79.9338' >>> row['time'] '09:15:00'
-
我们可以转换为具有固定顺序的数据结构。
row.items()
的值是每个键和值的一个可匹配的成对序列。订单不是预先设置的,但我们可以使用以下命令强制订单:>>> sorted(row.items())
这将返回一个按顺序排列键的列表。这允许我们创建一个一致的文本值,该值在每次评估测试时总是相同的。
大多数浮点实现都相当一致。然而,对于任何给定浮点数的最后几位,几乎没有正式的保证。与其相信所有 53 位的值都是正确的,通常更容易将值四舍五入到适合问题域的值。
对于大多数现代处理器,浮点值通常是 32 位或 64 位值。32 位值大约有七位十进制数字。将值四舍五入,使值中的数字不超过六位通常是最简单的方法。
四舍五入到六位数并不意味着使用round(x, 6)
。round()
函数不保留一定数量的数字。此函数舍入到小数点右侧的位数;它不考虑小数点左边的数字。将 1012数量级的数字四舍五入到小数点右侧的六个位置,对于 32 位值而言,会导致 18 位数字过多。将 10-7顺序的数字四舍五入到小数点右侧的六个位置即为零。
在处理set
对象时,我们还必须注意项目的顺序。我们通常可以使用sorted()
将set
转换为list
,并施加特定的顺序。
Pythondict
对象出现在数量惊人的地方:
- 当我们编写一个使用
**
收集参数值字典的函数时。无法保证论点的顺序。 - 当我们使用
vars()
之类的函数从局部变量或对象的属性创建字典时,字典没有保证的顺序。 - 当我们编写依赖于类定义的内省的程序时,方法是在类级字典对象中定义的。我们无法预测他们的订单。
当存在不可靠的测试用例时,这一点变得很明显。看似随机通过或失败的测试用例可能有一个基于散列随机化的结果。提取密钥并对其进行排序以解决此问题。
我们也可以使用此命令行选项运行测试:
python3.5 -R -m doctest ch11_r03.py
这将在对特定文件ch11_r03.py
运行 doctest 时关闭散列随机化。
- 涉及日期或时间的测试方法配方,尤其是日期时间的
now()
方法需要一些小心。 - 测试涉及随机性的事物配方将展示如何测试涉及
random
的过程。
我们可以在 docstring 示例中进行任何类型的单元测试。然而,有些事情在这样做的时候会变得非常乏味。
unittest
模块允许我们超越简单示例。这些测试依赖于测试用例类定义。TestCase
的子类可用于编写非常复杂和复杂的测试;这些测试可能比与 doctest 示例相同的测试更简单。
unittest
模块还允许我们在 docstring 之外打包测试。这对于文档中没有帮助的特殊复杂的角落案例测试非常有用。理想情况下,doctest 案例说明了**快乐路径—**最常见的用例。通常,对于偏离愉快路径的测试用例,使用unittest
是很常见的。
我们如何创建更复杂的测试?
一个测试通常可以总结为三个部分*,当*故事出现时:
- 给定:处于初始状态或上下文中的某个单元
- 当时:请求行为
- 然后:被测部件有某种预期结果或状态变化
TestCase
类并不完全遵循这三部分结构。它包括两部分;必须就测试的三个部分的分配位置做出一些设计选择:
- 实现测试用例的给定方面的
setUp()
方法。在方面也可以处理方面。 - 必须处理然后方面的
runTest()
方法。这也可以处理When方面。然后条件通过一系列断言得到确认。它们通常使用TestCase
类的复杂断言方法。
当方面与重用问题相关时,在何处实现方面的选择。在大多数情况下,当条件出现时,有许多备选条件,每个条件都有一个唯一的然后来确认正确的操作。中给出的可能是setUp()
方法的共同部分,并由许多TestCase
子类共享。每个子类都有一个独特的runTest()
方法来实现When和Then方面。
在某些情况下,When方面被划分为一些公共部分和一些特定于测试用例的部分。在这种情况下,When方面可以在setUp()
方法中部分定义,在runTest()
方法中部分定义。
我们将为一个类创建一些测试,该类用于计算一些基本的描述性统计数据。我们希望提供的样本数据远远大于作为 doctest 示例输入的任何数据。我们希望使用数千个数据点,而不是两三个。
下面是我们要测试的类定义的概要。我们只提供方法和一些总结。大部分代码显示在使用 docstring 测试配方的中。我们省略了所有的实现细节。这只是类的概要,作为方法名称的提醒:
from statistics import median
from collections import Counter
class Summary:
def __init__(self):
pass
def __str__(self):
'''Returns a multi-line text summary.'''
def add(self, value):
'''Adds a value to be summarized.'''
@property
def count(self):
'''Number of samples.'''
@property
def mean(self):
'''Mean of the collection.'''
@property
def median(self):
'''Median of the collection.'''
return median(self.counts.elements())
@property
def mode(self):
'''Returns the items in the collection in decreasing
order by frequency.
'''
因为我们没有查看实现细节,所以这是一种黑盒测试。代码是一个黑匣子,内部是不透明的。为了强调这一点,我们省略了前面代码中的实现细节。
我们希望确保,当我们使用数千个样本时,类能够正确执行。我们还希望确保它能够快速工作;我们将使用它作为整体性能测试以及单元测试的一部分。
-
We'll include the test code in the same module as the working code. This will follow the pattern of doctest that bundles tests and code together. We'll use the
unittest
module for creating test classes:import unittest import random
我们还将使用
random
对输入数据进行置乱。 -
Create a subclass of
unittest.TestCase
. Provide this class with a name that shows the intent of the test:class GIVEN_Summary_WHEN_1k_samples_THEN_mean(unittest.TestCase):
给定的 WHEN-THEN名称非常长。我们将依靠
unittest
来发现TestCase
的所有子类,因此我们不必多次键入此类名。 -
Define a
setUp()
method in this class which handles the Given aspect of the test. This creates a context for test processing:def setUp(self): self.summary = Summary() self.data = list(range(1001)) random.shuffle(self.data)
我们已经创建了一组
1,001
样本,其价值范围从0
到1,000
。平均值正好是 500,中位数也是。我们已将数据按随机顺序排列。 -
定义一个
runTest()
方法来处理测试的When方面。这将执行状态更改:def runTest(self): for sample in self.data: self.summary.add(sample)
-
添加断言以实现测试的然后方面。这确认状态更改工作正常:
self.assertEqual(500, self.summary.mean) self.assertEqual(500, self.summary.median)
-
To make it very easy to run, add a main program section:
if __name__ == "__main__": unittest.main()
这样,可以在命令提示符下运行测试。它也可以从命令行运行。
我们正在使用unittest
模块的几个部分:
-
The
TestCase
class is used to define one test case. This can have asetUp()
method to create the unit and possibly the request . This must have at least arunTest()
to make the request and check the response.我们可以在一个文件中包含任意数量的此类定义,以建立一组适当的测试。对于简单类,可能只有几个测试用例。对于复杂的模块,可能有几十种甚至数百种情况。
-
unittest.main()
函数完成以下几项功能:- 它创建一个空的
TestSuite
,其中包含所有的TestCase
对象。 - 它使用一个默认加载程序来检查一个模块并找到所有的
TestCase
实例。这些被加载到TestSuite
中。这个过程可能需要修改或扩展。 - 然后运行
TestSuite
并显示结果摘要。
- 它创建一个空的
运行此模块时,我们将看到如下输出:
.----------------------------------------------------------------------
Ran 1 test in 0.005s
OK
每次测试通过后,显示一个.
。这表明测试套件正在取得进展。-
行之后是测试运行的摘要,以及时间。如果出现故障或异常,计数将反映这一点。
最后,有一个OK
的摘要,显示所有测试是否通过或任何测试是否失败。
如果我们稍微更改测试以确保它失败,我们将看到以下输出:
F
======================================================================
FAIL: runTest (__main__.GIVEN_Summary_WHEN_1k_samples_THEN_mean)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/slott/Documents/Writing/Python Cookbook/code/ch11_r04.py", line 24, in runTest
self.assertEqual(501, self.summary.mean)
AssertionError: 501 != 500.0
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)
通过测试时不显示.
,失败测试时显示F
。然后是对失败的断言的回溯。为了迫使测试失败,我们将预期平均值更改为501
,这不是500.0
的计算平均值。
有一个FAILED
的最终总结。这包括套件整体失败的原因:(failures=1)
。
在本例中,runTest()
方法中有两个和条件。如果其中一个失败,测试将作为失败而停止,而另一个条件将不执行。
这是本测试设计中的一个弱点。如果第一次测试失败,我们将无法获得可能需要的所有诊断信息。我们应该避免在runTest()
方法中独立收集断言。在许多情况下,一个测试用例可能涉及多个依赖断言;单个故障提供所需的所有诊断信息。断言的集群是简单性和诊断细节之间的设计权衡。
当我们需要更多的诊断细节时,我们有两个常规选择:
- 使用多种测试方法代替
runTest()
。编写多个名称以test_
开头的方法。删除任何名为runTest()
的方法。在重新运行公共setUp()
方法后,默认测试加载程序将分别执行每个test_
方法。 - 使用
GIVEN_Summary_WHEN_1k_samples_THEN_mean
类的多个子类,每个子类都有一个单独的条件。因为setUp()
是常见的,所以可以继承。
按照第一个备选方案,测试类如下所示:
class GIVEN_Summary_WHEN_1k_samples_THEN_mean_median(unittest.TestCase):
def setUp(self):
self.summary = Summary()
self.data = list(range(1001))
random.shuffle(self.data)
for sample in self.data:
self.summary.add(sample)
def test_mean(self):
self.assertEqual(500, self.summary.mean)
def test_median(self):
self.assertEqual(500, self.summary.median)
我们重构了setUp()
方法,将给定的和测试条件时的包括在内。这两个独立的和条件被重构为各自独立的test_mean()
和test_median()
方法。没有runTest()
方法。
由于每个测试都是单独运行的,我们将看到计算平均值或计算中值问题的单独错误报告。
TestCase
类定义了许多断言,这些断言可以用作然后条件的一部分;以下是一些最常用的方法:
assertEqual()
和assertNotEqual()
使用默认的==
运算符比较实际值和预期值。assertTrue()
和assertFalse()
需要一个布尔表达式。assertIs()
和assertIsNot()
使用is
比较来确定这两个参数是否引用了同一个对象。assertIsNone()
和assertIsNotNone()
使用is
将给定值与None
进行比较。assertIsInstance()
和assertNotIsInstance()
使用isinstance()
函数确定给定值是否为给定类(或类的元组)的成员。assertAlmostEquals()
和assertNotAlmostEquals()
将给定值四舍五入至小数点后七位,以查看大多数数字是否相等。assertRegex()
和assertNotRegex()
使用正则表达式比较给定字符串。这使用正则表达式的search()
方法来匹配字符串。assertCountEqual()
比较两个序列,看它们是否具有相同的元素,而不考虑顺序。这对于比较字典键和集合也很方便。
还有更多的断言方法。其中许多提供了检测异常、警告和日志消息的方法。另一个组提供更多类型特定的比较功能。
例如,Summary
类的模式特性生成一个列表。我们可以使用特定的assertListEqual()
断言来比较结果:
class GIVEN_Summary_WHEN_1k_samples_THEN_mode(unittest.TestCase):
def setUp(self):
self.summary = Summary()
self.data = [500]*97
# Build 993 more elements each item n occurs n times.
for i in range(1,43):
self.data += [i]*i
random.shuffle(self.data)
for sample in self.data:
self.summary.add(sample)
def test_mode(self):
top_3 = self.summary.mode[:3]
self.assertListEqual([(500,97), (42,42), (41,41)], top_3)
首先,我们构建了 1000 个值的集合。其中 97 份是数字 500 的副本。剩下的 903 个元素是介于 1 和 42 之间的数字的副本。这些数字有一个简单的规则,频率就是值。这条规则使确认结果更容易。
setUp()
方法将数据随机排序。然后使用add()
方法构建Summary
对象。
我们使用了一种test_mode()
方法。这允许扩展以包括本测试中的其他和条件。在本例中,我们检查了模式中的前三个值,以确保其具有预期的值分布。assertListEqual()
比较两个list
对象;如果任一参数不是列表,我们将得到一条更具体的错误消息,表明该参数不是预期的类型。
我们已经在与测试代码相同的模块中显示了TestCase
类定义。对于小班来说,这是很有帮助的。与该类相关的所有内容都可以在一个模块文件中找到。
在大型项目中,通常的做法是将测试文件隔离到单独的目录中。这些测试可能(通常)非常大。测试代码多于应用程序代码并非不合理。
完成后,我们可以依赖unittest
框架中的发现应用程序。此应用程序可以在给定目录的所有文件中搜索测试文件。通常,这些文件的名称与模式test*.py
匹配。如果我们对所有测试模块使用一个简单、一致的名称,那么可以使用一个简单的命令来定位和运行它们。
unittest
加载程序将在目录中的每个模块中搜索从TestCase
类派生的所有类。较大模块集合中的此类集合成为完整的TestSuite
。我们可以通过os
命令执行此操作:
$ python3 -m unittest discover -s tests
这将在项目的tests
目录中找到所有测试。
- 我们将在组合单元测试和博士测试配方中组合
unittest
和doctest
。我们将在模拟外部资源配方中研究模拟外部对象。
在大多数情况下,我们将结合使用unittest
和doctest
测试用例。有关 doctest 的示例,请参阅使用 docstring 进行测试的配方。有关 unittest 的示例,请参见创建单独的测试模块和包配方。
doctest
示例是模块、类、方法和函数文档字符串的基本元素。unittest
案例通常位于单独的tests
目录中,文件名与模式test_*.py
匹配。
我们如何将所有这些不同的测试组合成一个整洁的包?
我们将参考中使用 docstring 测试配方的示例。这个配方为一个类Summary
创建了测试,该类进行一些统计计算。在该配方中,我们在文档字符串中包含了示例。
课程是这样开始的:
class Summary:
'''Computes summary statistics.
>>> s = Summary()
>>> s.add(8)
>>> s.add(9)
>>> s.add(9)
>>> round(s.mean, 2)
8.67
>>> s.median
9
>>> print(str(s))
mean = 8.67
median = 9
'''
这里省略了这些方法,以便我们可以关注 docstring 中提供的示例。
在创建单独的测试模块和包配方中,我们编写了一些unittest.TestCase
类来为这个类提供额外的测试。我们创建了如下类定义:
class GIVEN_Summary_WHEN_1k_samples_THEN_mean_median(unittest.TestCase):
def setUp(self):
self.summary = Summary()
self.data = list(range(1001))
random.shuffle(self.data)
for sample in self.data:
self.summary.add(sample)
def test_mean(self):
self.assertEqual(500, self.summary.mean)
def test_median(self):
self.assertEqual(500, self.summary.median)
此测试创建一个Summary
对象;这是给定的方面。然后,它向该Summary
对象添加许多值。这是测试的When方面。这两个test_
方法实现了本测试的两个和方面。
常见的项目文件夹结构如下所示:
git-project-name/
statstools/
summary.py
tests/
test_summary.py
我们有一个顶级文件夹git-project-name
,它与源代码存储库中的项目名称相匹配。我们假设正在使用 Git,但也可以使用其他工具。
在顶层目录中,我们会有一些大型 Python 项目常见的开销。这将包括带有项目说明的文件README.rst``requirements.txt
,可与pip
一起使用以安装额外的软件包,也可能与setup.py
一起将软件包安装到标准库中。
目录statstools
包含一个模块文件summary.py
。这是我们的模块,提供了有趣和有用的功能。此模块在代码中有分散的 docstring 注释。
目录tests
包含另一个模块文件test_summary.py
。这里面有unittest
测试用例。我们选择了 tests 和test_*.py
的名称,以便它们与自动测试发现相匹配。
我们需要将所有的测试组合成一个单一的、全面的测试套件。
我们将展示的示例使用了ch11_r01
而不是一些更酷的名称,例如summary
。一个真正的项目通常有聪明、有意义的名字。这本书的内容相当丰富,名字的设计与整个章节和食谱大纲相匹配。
-
For this example, we'll assume that the unittest test cases are in a file separate from the code being tested. We'll have
ch11_r01
andtest_ch11_r01
.要使用 doctest 测试,请导入
doctest
模块。我们将结合 doctest 示例和TestCase
类来创建一个全面的测试套件:import unittest import doctest
我们假设
unittest``TestCase
类已经就位,并且我们正在向测试套件添加更多测试。 -
导入正在测试的模块。此模块将包含包含 doctest 的字符串:
import ch11_r01
-
To implement the
load_tests
protocol, include the following function in the test module:def load_tests(loader, standard_tests, pattern): return standard_tests
该函数必须具有此名称才能由测试加载程序找到。
-
To incorporate doctest tests, an additional loader is required. We'll use the
doctest.DocTestSuite
class to create a suite. These tests will be added to the suite of tests provided as thestandard_tests
parameter value:def load_tests(loader, standard_tests, pattern): dt = doctest.DocTestSuite(ch11_r01) standard_tests.addTests(dt) return standard_tests
loader
参数是当前正在使用的测试用例加载程序。standard_tests
值将是默认加载的所有测试。通常,这是TestCase
所有子类的集合。模式值是提供给加载程序的值。
我们现在可以添加TestCase
类和整体unittest.main()
函数来创建一个综合测试模块,其中包括 unittestTestCase
和所有 doctest 示例。
这可以通过包含以下代码来实现:
if __name__ == "__main__":
unittest.main()
这允许我们运行模块并执行测试。
当我们在该模块内评估unittest.main()
时,测试加载程序流程仅限于当前模块。加载程序将查找扩展TestCase
的所有类。这些是为load_tests()
功能提供的标准测试。
我们将使用doctest
模块创建的测试来补充标准测试。通常,我们可以导入正在测试的模块,并使用DocTestSuite
从导入的模块构建测试套件。
load_tests()
功能由unittest
模块自动使用。此函数可以对给定的测试套件执行多种操作。在本例中,我们用附加测试补充了测试套件。
在某些情况下,模块可能相当复杂;这可能导致多个测试模块。可能有几个名为tests/test_module_feature.py
或类似名称的测试模块,以表明对一个复杂模块的单独功能有多个测试。
在其他情况下,我们可能有一个测试模块,该模块对几个不同但密切相关的模块进行测试。一个包可以分解为多个模块。但是,单个测试模块可能涵盖被测试包中的所有模块。
当组合许多较小的模块时,load_tests()
功能中可能内置多个套件。身体可能看起来像这样:
def load_tests(loader, standard_tests, pattern):
for module in ch11_r01, ch11_r02, ch11_r03:
dt = doctest.DocTestSuite(module)
standard_tests.addTests(dt)
return standard_tests
这将包含来自多个模块的doctests
。
- 有关 doctest 的示例,请参见使用 docstrings 测试配方。有关 unittest 的示例,请参见创建单独的测试模块和包配方。
许多应用程序依赖datetime.datetime.now()
来创建时间戳。当我们将其用于单元测试时,结果基本上是无法预测的。我们这里有一个依赖注入问题,我们的应用程序依赖于一个类,我们只想在测试时替换这个类。
一种选择是避免now()
和utcnow()
。我们可以创建一个发出时间戳的工厂函数,而不是直接使用这些函数。出于测试目的,此函数可以替换为产生已知结果的函数。在复杂的应用程序中避免使用now()
方法似乎有些尴尬。
另一个选择是完全避免直接使用datetime
类。这需要设计封装datetime
类的类和模块。然后可以使用为now()
生成已知值的包装器类进行测试。这似乎也不必要地复杂。
我们如何处理datetime
邮票?
我们将使用一个创建CSV
文件的小函数。此文件的名称将包括日期和时间。我们将创建名称如下所示的文件:
extract_20160704010203.json
长时间运行的服务器应用程序可能会使用这种文件命名约定。该名称有助于匹配文件和相关日志事件。它可以帮助跟踪服务器正在完成的工作。
我们将使用如下函数创建这些文件:
import datetime
import json
from pathlib import Path
def save_data(some_payload):
now_date = datetime.datetime.utcnow()
now_text = now_date.strftime('extract_%Y%m%d%H%M%S')
file_path = Path(now_text).with_suffix('.json')
with file_path.open('w') as target_file:
json.dump(some_payload, target_file, indent=2)
此功能使用了utcnow()
。从技术上讲,重新设计函数并提供时间戳作为参数是可能的。在某些情况下,这种重新设计可能会有所帮助。除了重新设计,还有一个方便的替代方案。
我们将创建datetime
模块的模拟版本,并修补测试上下文以使用模拟版本而不是实际版本。此测试将包含datetime
类的模拟类定义。在该类中,我们将提供一个 mockutcnow()
方法,该方法将提供预期的响应。
由于正在测试的函数会创建一个文件,因此我们需要考虑这样做对操作系统的影响。如果已经存在同名文件,会发生什么情况?是否应该提出例外?是否应在文件名中添加后缀?根据我们的设计决策,我们可能需要两个额外的测试用例:
- 给定一个没有冲突的目录。在这种情况下,需要一种
setUp()
方法来删除任何先前的测试输出。我们可能还想创建一个tearDown()
方法,在测试后删除该文件。 - 给定一个名称冲突的目录。在这种情况下,
setUp()
方法将创建一个冲突文件。我们可能还想创建一个tearDown()
方法,在测试后删除该文件。
对于这个配方,我们假设重复的文件名并不重要。新文件应该只覆盖任何以前的文件,而不发出警告或通知。这很容易实现,并且通常适合于在时间间隔不到 1 秒的情况下创建多个文件的真实场景。
-
For this example, we'll assume that the
unittest
test cases are the same module as the code being tested. Import theunittest
andunittest.mock
modules:import unittest from unittest.mock import *
unittest
模块简单导入。要使用此模块的功能,我们必须使用unittest.
限定名称。unittest.mock
中的各种名称都已导入,因此可以使用这些名称而无需任何限定符。我们将使用 mock 模块的许多特性,而冗长的限定名很难使用。 -
包括要测试的代码。如前所示。
-
Create the following skeleton for testing. We've provided one class definition, plus a main script that can be used to execute the tests:
class GIVEN_data_WHEN_save_data_THEN_file(unittest.TestCase): def setUp(self): '''GIVEN conditions for the test.''' def runTest(self): '''WHEN and THEN conditions for this test.'''' if __name__ == "__main__": unittest.main()
我们没有定义
load_tests()
函数,因为我们没有任何要包含的 docstring 测试。 -
setUp()
方法将有几个部分:-
待处理样本数据:
self.data = {'primes': [2, 3, 5, 7, 11, 13, 17, 19]}
-
datetime
模块的模拟对象。该对象精确地提供了被测单元所使用的特性。Mock
模块包含datetime
类的单个Mock
类定义。在该类中,它提供了一个模拟方法utcnow()
,该方法始终提供相同的响应:self.mock_datetime = Mock( datetime = Mock( utcnow = Mock( return_value = datetime.datetime(2017, 7, 4, 1, 2, 3) ) ) )
-
下面是上面显示的
datetime
对象的预期文件名:self.expected_name = 'extract_20170704010203.json'
-
需要一些额外的配置处理来建立给定的*条件。我们将删除该文件的任何以前版本,以完全确保测试断言没有使用以前测试运行中的文件:
self.expected_path = Path(self.expected_name) if self.expected_path.exists(): self.expected_path.unlink() ```*
-
-
runTest()
方法将分为两部分:with self.expected_path.open() as result_file: result_data = json.load(result_file) self.assertDictEqual(self.data, result_data)
-
处理时的*。这将修补当前模块
__main__
,因此对datetime
的引用将替换为self.mock_datetime
对象。然后,它将在该修补上下文中执行请求:with patch('__main__.datetime', self.mock_datetime): save_data(self.data) ```*
-
然后处理。在本例中,我们将打开预期的文件,加载内容,并确认结果与源数据匹配。最后是必要的断言。如果文件不存在,将引发
IOError
异常:
-
unittest.mock
模块有两个有价值的组件,我们在这里使用Mock
对象定义和patch()
函数。
当我们创建Mock
类的实例时,我们必须提供结果对象的方法和属性。当我们提供一个命名参数值时,它将被保存为结果对象的属性。简单值将成为对象的属性。基于Mock
对象的值成为方法函数。
当我们创建一个提供return_value
(或side_effect
)命名参数值的Mock
实例时,我们正在创建一个可调用对象。下面是一个模拟对象的示例,其行为类似于一个非常愚蠢的函数:
>>> from unittest.mock import *
>>> dumb_function = Mock(return_value=12)
>>> dumb_function(9)
12
>>> dumb_function(18)
12
我们创建了一个模拟对象dumb_function
,它的行为类似于一个可调用的函数—一个只返回值12
的函数。对于单元测试,这非常方便,因为结果简单且可预测。
更重要的是Mock
对象的这个特性:
>>> dumb_function.mock_calls
[call(9), call(18)]
dumb_function()
跟踪了每一次通话。然后我们可以对这些调用做出断言。例如,assert_called_with()
方法检查历史记录中的最后一个调用:
>>> dumb_function.assert_called_with(18)
如果最后一个调用真的是dumb_function(18)
,那么这将以静默方式成功。如果最后一个调用与断言不匹配,则会引发一个异常,unittest
模块将捕获并注册为测试失败。
我们可以看到更多细节如下:
>>> dumb_function.assert_has_calls( [call(9), call(18)] )
此断言检查整个调用历史记录。它使用Mock
模块中的call()
函数来描述函数调用中提供的参数。
patch()
函数可以进入模块的上下文并更改该上下文中的任何引用。在本例中,我们使用patch()
调整__main__
模块中当前正在运行的定义。在许多情况下,我们将导入另一个模块,并且需要修补该导入的模块。重要的是要接触到对被测模块有效的上下文,并修补该引用。
在本例中,我们为datetime
模块创建了一个模拟,该模块具有非常窄的特性集。
模块有一个元素,它是Mock
类的一个实例,名为datetime
。出于单元测试的目的,模拟类的行为通常类似于返回对象的函数。在本例中,类返回了一个Mock
对象。
代表datetime
类的Mock
对象只有一个属性utcnow()
。我们在定义这个属性时使用了特殊的return_value
关键字,这样它将返回一个固定的datetime
实例。我们可以扩展此模式并模拟多个属性,使其行为类似于函数。下面是一个模仿utcnow()
和now()
的例子:
self.mock_datetime = Mock(
datetime = Mock(
utcnow = Mock(
return_value = datetime.datetime(2017, 7, 4, 1, 2, 3)
),
now = Mock(
return_value = datetime.datetime(2017, 7, 4, 4, 2, 3)
)
)
)
两个模拟方法utcnow()
和now()
各自创建一个不同的datetime
对象。这使我们能够区分这些值。我们可以更容易地确认单元测试的正确操作。
请注意,所有这些Mock
对象构造都是在setUp()
方法期间执行的。这比patch()
函数完成修补要早得多。在setUp()
期间,datetime
类可用。在with
语句的上下文中,datetime
类不可用,由Mock
对象替换。
我们可以添加以下断言,以确认被测单元正确使用了utcnow()
功能:
self.mock_datetime.datetime.utcnow.assert_called_once_with()
这将检查self.mock_datetime
模拟对象。它在这个对象内部查看datetime
属性,我们已经定义了utcnow
属性。我们希望它只调用一次,没有参数值。
如果save_data()
函数没有正确调用utcnow()
,此断言将检测到该故障。测试接口的两侧是很重要的。这将导致测试分为两部分:
- 模拟
datetime
的结果被测试单元正确使用 - 被测试单元对模拟的
datetime
对象提出了适当的请求
在某些情况下,我们可能需要确认从未调用过时或不推荐的方法。我们可能会使用类似的方法来确认未使用其他方法:
self.assertFalse( self.mock_datetime.datetime.called )
这种测试在重构软件时使用。在此示例中,以前的版本可能使用了now()
方法。更改后,该功能需要使用utcnow()
方法。我们已经包括了一项测试,以确保不再使用now()
方法。
- 创建单独的测试模块和包配方中有更多关于
unittest
模块基本用途的信息
许多应用程序依赖于random
模块来创建随机值或将值放入随机顺序。在许多统计测试中,重复进行随机洗牌或随机子集计算。当我们想要测试其中一种算法时,结果基本上是无法预测的。
为了使random
模块具有足够的可预测性以编写有意义的单元测试,我们有两种选择:
- 设置一个已知的种子值,这是很常见的,我们已经在许多其他食谱中大量使用了它。
- 使用
unittest.mock
将random
模块替换为随机性更小的模块。
我们如何对涉及随机性的算法进行单元测试?
给定一个样本数据集,我们可以计算一个统计指标,如平均值或中位数。下一个常见步骤是确定某些总体人口的这些统计指标的可能值。这可以通过一种称为引导的技术来实现。
其思想是重复对初始数据集进行重采样。每一次重采样都提供了对统计度量的不同估计。这一整套重采样指标显示了总体人口的测量可能存在的差异。
为了确保重采样算法有效,它有助于消除处理过程中的随机性。我们可以使用非随机版本的random.choice()
函数对精心设计的数据集进行重采样。如果这能正常工作,那么我们有理由相信一个真正的随机版本也能工作。
这是我们的候选重采样函数。我们需要对其进行验证,以确保其能够正确地进行替换取样:
def resample(population, N):
for i in range(N):
sample = random.choice(population)
yield sample
我们通常会应用这个resample()
函数来填充一个Counter
对象,该对象跟踪特定度量值(如平均值)的每个不同值。整个重采样过程如下所示:
mean_distribution = Counter()
for n in range(1000):
subset = list(resample(population, N))
measure = round(statistics.mean(subset), 1)
mean_distribution[measure] += 1
这将评估resample()
功能1,000
次。这将产生许多子集,每个子集的平均值可能不同。这些值用于填充mean_distribution
对象。
mean_distribution
的直方图将为总体方差提供有意义的估计。这种方差估计将有助于显示总体最可能的实际平均值。
-
Define an outline of the overall test class:
class GIVEN_resample_WHEN_evaluated_THEN_fair(unittest.TestCase): def setUp(self): def runTest(self): if __name__ == "__main__": unittest.main()
我们包含了一个主程序,这样我们就可以简单地运行模块来测试它。这在使用诸如 IDLE 之类的工具时非常方便;更改后,我们可以使用F5键测试模块。
-
Define a mock version of the
random.choice()
function. We'll provide a mock data set,self.data
, and a mock response to thechoice()
function:self.expected_resample_data.self.data = [2, 3, 5, 7, 11, 13, 17, 19] self.expected_resample_data = [23, 29, 31, 37, 41, 43, 47, 53] self.mock_random = Mock( choice = Mock( side_effect = self.expected_resample_data ) )
我们已经使用
side_effect
属性定义了choice()
函数。这将从给定序列中一次返回一个值。我们已经提供了八个不同于源序列的模拟值,因此我们可以很容易地识别来自choice()
函数的输出。 -
定义测试的时和时方面。在这种情况下,我们将修补
__main__
模块以替换对random
模块的引用。然后,测试可以确定结果具有预期的值集,并且多次调用了choice()
函数:with patch('__main__.random', self.mock_random): resample_data = list(resample(self.data, 8)) self.assertListEqual(self.expected_resample_data, resample_data) self.mock_random.choice.assert_has_calls( 8*[call(self.data)] )
当我们创建Mock
类的实例时,我们必须提供结果对象的方法和属性。当Mock
对象包含命名参数值时,该值将保存为结果对象的属性。
当我们创建一个提供命名参数值的Mock
实例时,我们正在创建一个可调用对象。每次调用Mock
对象时,可调用对象将从side_effect
列表返回一个值。
下面是一个模拟对象的示例,其行为类似于一个非常愚蠢的函数:
>>> from unittest.mock import *
>>> dumb_function = Mock(side_effect=[11,13])
>>> dumb_function(23)
11
>>> dumb_function(29)
13
>>> dumb_function(31)
Traceback (most recent call last):
... (traceback details omitted)
StopIteration
首先,我们创建了一个Mock
对象并将其分配给名称dumb_function
。此Mock
对象的side_effect
属性提供了将返回的两个不同值的简短列表。
然后,该示例使用两个不同的参数值对dumb_function()
进行两次计算。每次从side_effect
列表返回下一个值。第三次尝试引发StopIteration
异常,该异常将成为测试失败。
这种行为允许我们编写一个测试来检测函数或方法的某些不正确使用。如果函数调用次数过多,将引发异常。其他不正确的使用必须通过可用于Mock
对象的各种断言来检测。
我们可以很容易地用模拟对象替换random
模块的其他功能,这些模拟对象提供适当的行为,而实际上不是随机的。例如,我们可以用提供已知顺序的函数替换shuffle()
函数。我们可以遵循上面的测试设计模式,如下所示:
self.mock_random = Mock(
choice = Mock(
side_effect = self.expected_resample_data
),
shuffle = Mock(
return_value = self.expected_resample_data
)
)
此模拟shuffle()
函数返回一组不同的值,可用于确认某些进程是否正确使用了random
模块。
- 使用 set 方法和运算符、创建字典第 4 章中插入和更新配方、内置数据结构【列表、集合、dict】、使用 cmd 在中创建命令行应用程序配方第 5 章用户输入和输出展示了如何为随机数生成器播种,以创建可预测的值序列。
- 在第 6 章、类和对象的基础中,还有其他几种方法展示了替代方法,例如使用类封装数据+处理、设计具有大量处理的类、使用[uuuu 插槽[uuu9]和使用惰性属性的属性优化小对象。
- 另外,在第 7 章更高级的类设计中,请参见在继承和扩展之间的选择——is-a 问题、通过多重继承分离关注点、利用 Python 的 duck 类型、创建一个具有可排序对象的类,以及定义有序集合配方。
涉及日期或时间的测试和涉及随机性的测试配方展示了模拟相对简单对象的技术。在测试涉及日期或时间配方的事情的情况下,被模拟的对象基本上是无状态的,并且单个返回值可以很好地工作。在测试涉及随机性的事物配方中,对象有状态变化,但状态变化不依赖于任何输入参数。
在这些简单的情况下,测试向对象提供一系列请求。模拟对象可以基于已知且经过仔细规划的状态更改序列来构建。测试用例精确地跟踪对象的内部状态变化。这有时被称为白盒测试,因为定义测试序列和模拟对象需要测试对象的实现细节。
然而,在某些情况下,测试场景可能不涉及定义良好的状态变化序列。被测单元可能会以难以预测的顺序发出请求。这有时是黑盒测试的结果,其中实现细节未知。
我们如何才能创建更复杂的模拟对象,这些对象具有内部状态并进行自己的内部状态更改?
我们将研究模拟有状态 RESTful web 服务请求。在本例中,我们将为弹性数据库使用数据库 API。参见https://www.elastic.co/ 了解有关此数据库的更多信息。该数据库的优点是使用简单的 RESTful web 服务。这些可以很容易地模拟为简单、快速的单元测试。
对于这个配方,我们将测试一个使用 RESTful API 创建记录的函数。表征状态转移(REST)是一种使用超文本转移协议(HTTP)在进程之间转移对象状态表示的技术。例如,为了创建数据库记录,客户机将使用 HTTPPOST
请求将对象状态的表示形式传输到数据库服务器。在许多情况下,JSON 符号用于表示对象状态。
测试此功能需要模拟urllib.request
模块的一部分。替换urlopen()
函数将允许测试用例模拟数据库活动。这将允许我们测试一个依赖于 web 服务的函数,而不需要实际发出潜在的昂贵或缓慢的外部请求。
在我们的应用软件中使用弹性搜索 API 有两种总体方法:
-
We can install the Elastic database on our laptop or some server to which we have access. The installation is a two-part process that starts by installing a proper Java Developer Kit ( JDK ) and then installs the ElasticSearch software. We won't go into details here, because we have an alternative which seems simpler.
在本地计算机上创建和访问对象的 URL 如下所示:
http://localhost:9200/eventlog/event/
请求将使用请求正文中的许多数据项。出于安全或身份验证目的,这些请求不需要任何 HTTP 头。
-
We can use a hosting service such as http://orchestrate.io . This requires signing up with the service to get an API key instead of installing software. An API key grants access to a defined application. Within the application, a number of collections can be created. Since we won't have to install additional software, this seems like a handy way to proceed.
在远程服务器上使用对象的 URL 如下所示:
https://api.orchestrate.io/v0/eventlog/
请求还将使用大量 HTTP 头向主机提供信息。接下来,我们将了解此服务的详细信息。
要创建的文档的数据有效负载如下所示:
{
"timestamp": "2016-06-15T17:57:54.715",
"levelname": "INFO",
"module": "ch09_r10",
"message": "Sample Message One"
}
此 JSON 文档表示一个日志条目。这来自前面示例中使用的sample.log
文件。此文档可以理解为将保存在数据库的eventlog
索引中的事件类型的特定实例。该对象有四个具有字符串值的属性。
第 9 章输入/输出、物理格式和逻辑布局中的使用正则表达式读取复杂格式配方,展示了如何解析复杂日志文件。在使用多个上下文读取和写入文件配方中,将复杂的日志记录写入CSV
文件。在本例中,我们将展示如何使用数据库(如 Elastic)将日志记录放入基于云的存储中。
我们将在数据库的entrylog
集合中创建条目文档。HTTPPOST
请求用于创建新项目。201 Created
的响应将指示数据库创建了新事件。
要使用orchestrate.io
数据库服务,每个请求都有一个基本 URL。我们可以用如下字符串来定义:
service = "https://api.orchestrate.io"
使用https
方案,使用安全套接字层(SSL)来确保客户端和服务器之间的数据是私有的。主机名为api.orchestrate.io
。每个请求都有一个基于此基本服务定义的 URL。
每个请求的 HTTP 头如下所示:
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': basic_header(api_key, '')
}
Accept
标题显示预期的响应类型。Content-Type
标题显示内容所使用的文档表示形式。这两个头指示数据库对对象状态使用 JSON 表示。
Authorization
头是 API 密钥的发送方式。此标头的值是一个相当复杂的字符串。构建编码的 API 密钥字符串代码最简单,如下所示:
import base64
def basic_header(username, password):
combined_bytes = (username + ':' + password).encode('utf-8')
encoded_bytes = base64.b64encode(combined_bytes)
return 'Basic ' + encoded_bytes.decode('ascii')
这段代码将用户名和密码组合成单个字符串,然后使用UTF-8
编码方案将这些字符编码成字节流。base64
模块创建第二个字节流。在这个输出流中,四个字节将包含组成三个输入字节的位。字节从简化的字母表中选择。然后将该值与关键字'Basic '
一起转换回 Unicode 字符。此值可与Authorization
标题一起使用。
通过创建Request
对象,使用 RESTful API 最简单。该类在urllib.request
模块中定义。Request
对象组合了数据、URL 和头,并命名了一个特定的 HTTP 方法。下面是创建Request
实例的代码:
data_document = {
"timestamp": "2016-06-15T17:57:54.715",
"levelname": "INFO",
"module": "ch09_r10",
"message": "Sample Message One"
}
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': basic_header(api_key, '')
}
request = urllib.request.Request(
url=service + '/v0/eventlog',
headers=headers,
method='POST',
data=json.dumps(data_document).encode('utf-8')
)
请求对象包括四个元素:
url
参数的值是基本服务 URL 加上集合名称/v0/eventlog
。路径中的v0
是每个请求必须提供的版本信息。headers
参数包括Authorization
标头,该标头具有授权访问应用程序的 API 密钥。POST
方法将在数据库中创建一个新对象。data
参数为要保存的单据。我们已经将 Python 对象转换为 JSON 表示法中的字符串。然后使用UTF-8
编码将 Unicode 字符编码为字节。
处理包括发送请求和接收响应。urlopen()
函数接受Request
对象作为参数;这将生成传输到数据库服务器的请求。来自数据库服务器的响应将包括三个元素:
-
地位。这包括数字代码和原因字符串。创建文档时,预期响应代码为
201
,字符串为CREATED
。对于许多其他请求,代码是200
,字符串是OK
。 -
The response will also have headers. For a creation request, these will include the following:
[ ('Content-Type', 'application/json'), ('Location', '/v0/eventlog/12950a87ef024e43/refs/8e50b6bfc50b2dfa'), ('ETag', '"8e50b6bfc50b2dfa"'), ... ]
Content-Type
头告诉我们内容是用 JSON 编码的。Location
头提供了一个 URL,可用于检索所创建的对象。它还提供了一个ETag
头,它是对象当前状态的哈希摘要;这有助于支持缓存对象的本地副本。可能存在其他标题;我们刚刚在示例中展示了...
。 -
响应可能有一个主体。如果存在,这将是从数据库检索的 JSON 编码文档。必须使用响应的
read()
方法读取正文。一个物体可以相当大;Content-Length
头提供确切的字节数。
我们将为数据库访问定义一个简单的类。一个类可以为多个相关操作提供上下文和状态信息。当使用弹性数据库时,access 类可以只创建一次请求头字典,并在多个请求中重用它。
以下是数据库客户机类的本质。我们将在几个部分中展示这一点。首先,整体类定义:
class ElasticClient:
service = "https://api.orchestrate.io"
这定义了一个类级变量service
,带有 scheme 和 hostname。初始化方法__init__()
可以构建各种数据库操作所使用的头文件:
def __init__(self, api_key, password=''):
self.headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': ElasticClient.basic_header(api_key, password),
}
此方法获取 API 密钥并创建一组依赖于 HTTP 基本授权的头。业务流程服务未使用密码。但是,我们之所以包含它,是因为用户名和密码用于示例单元测试用例。
方法如下:
@staticmethod
def basic_header(username, password=''):
"""
>>> ElasticClient.basic_header('Aladdin', 'OpenSesame')
'Basic QWxhZGRpbjpPcGVuU2VzYW1l'
"""
combined_bytes = (username + ':' + password).encode('utf-8')
encoded_bytes = base64.b64encode(combined_bytes)
return 'Basic ' + encoded_bytes.decode('ascii')
此函数可以将用户名和密码组合起来为 HTTPAuthorization
头创建值。orchestrate.io
API 使用分配的 API 密钥作为用户名;密码是一个零长度字符串,''
。API 密钥是在有人注册其服务时分配的。免费的服务级别允许合理数量的事务和舒适的小型数据库。
我们以 docstring 的形式包含了一个单元测试用例。这提供了结果正确的证据。测试用例来自 HTTP 基本身份验证的 Wikipedia 页面。
最后一部分是将一个数据项加载到数据库的eventlog
集合中的方法:
def load_eventlog(self, data_document):
request = urllib.request.Request(
url=self.service + '/v0/eventlog',
headers=self.headers,
method='POST',
data=json.dumps(data_document).encode('utf-8')
)
with urllib.request.urlopen(request) as response:
assert response.status == 201, "Insertion Error"
response_headers = dict(response.getheaders())
return response_headers['Location']
此函数使用四条必需信息构建一个Request
对象:完整 URL、HTTP 头、方法字符串和编码数据。在本例中,数据编码为 JSON 字符串,JSON 字符串使用UTF-8
编码方案编码为字节。
评估urlopen()
函数发送请求并检索响应对象。此对象用作上下文管理器。with
语句确保资源被正确释放,即使在响应处理期间出现异常。
POST
方法的响应状态应为201.
任何其他状态都是问题。在此代码中,状态通过一个assert
语句进行检查。最好提供一条消息,如Expected 201 status, got {}.format(response.status)
。
然后检查标题以获得Location
标题。这提供了一个 URL 片段,用于定位创建的对象。
-
创建数据库访问模块。此模块将具有
ElasticClient
类定义。它还将具有该类需要的任何其他定义。 -
此配方将使用
unittest
和doctest
创建一套统一的测试。它将使用unittest.mock
中的Mock
类以及json
类。由于该模块与被测单元是分离的,因此需要导入ch11_r08_load
,该模块有待测的类定义:import unittest from unittest.mock import * import doctest import json import ch11_r08_load
-
下面是测试用例的总体框架。我们将在下面填写本次测试的
setUp()
和runTest()
方法。名称显示我们得到了一个ElasticClient
实例,当我们调用load_eventlog()
时,发出了一个正确的 RESTful API 请求:class GIVEN_ElasticClient_WHEN_load_eventlog_THEN_request(unittest.TestCase): def setUp(self): def runTest(self):
-
The first part of the
setUp()
method is a mock context manager that provides responses similar to theurlopen()
function:def setUp(self): # The context manager object itself. self.mock_context = Mock( __exit__ = Mock(return_value=None), __enter__ = Mock( side_effect = self.create_response ), ) # The urlopen() function that returns a context. self.mock_urlopen = Mock( return_value = self.mock_context, )
调用
urlopen()
时,返回值是一个响应对象,其行为类似于上下文管理器。模拟此操作的最佳方法是返回模拟上下文管理器。模拟上下文管理器的__enter__()
方法完成了创建响应对象的实际工作。在这种情况下,side_effect
属性标识一个助手函数,该函数将被调用以准备调用__enter__()
方法的结果。self.create_response
尚未定义。我们将使用一个函数,定义如下。 -
The second part of the
setUp()
method is some mock data to be loaded:# The test document. self.document = { "timestamp": "2016-06-15T17:57:54.715", "levelname": "INFO", "module": "ch09_r10", "message": "Sample Message One" }
在一个更复杂的测试中,我们可能想要模拟一个大的、可编辑的文档集合。
-
Here's the
create_response()
helper method that builds response-like objects. The response objects can be complex, so we've defined a function to create them:def create_response(self): self.database_id = hex(hash(self.mock_urlopen.call_args[0][0].data))[2:] self.location = '/v0/eventlog/{id}'.format(id=self.database_id) response_headers = [ ('Location', self.location), ('ETag', self.database_id), ('Content-Type', 'application/json'), ] return Mock( status = 201, getheaders = Mock(return_value=response_headers) )
此方法使用
self.mock_urlopen.call_args
检查对该Mock
对象的最后一次调用。此调用的参数是位置参数值和关键字参数的元组。第一个[0]
索引从元组中选取位置参数值。第二个[0]
索引选择第一个位置参数值。这将是要加载到数据库的对象。hex()
函数的值是一个字符串,其中包含一个我们丢弃的0x
前缀。在更复杂的测试中,此方法可能需要保留加载到数据库中的对象缓存,以生成更精确的类似数据库的响应。
-
The
runTest()
method makes a patch to the module under test. It locates the reference fromch11_r08_load
tourllib.request
and to theurlopen()
function. This is replaced with themock_urlopen
replacement:def runTest(self): with patch('ch11_r08_load.urllib.request.urlopen', self.mock_urlopen): client = ch11_r08_load.ElasticClient('Aladdin', 'OpenSesame') response = client.load_eventlog(self.document) self.assertEqual(self.location, response) call_request = self.mock_urlopen.call_args[0][0] self.assertEqual( 'https://api.orchestrate.io/v0/eventlog', call_request.full_url) self.assertDictEqual( {'Accept': 'application/json', 'Authorization': 'Basic QWxhZGRpbjpPcGVuU2VzYW1l', 'Content-type': 'application/json' }, call_request.headers) self.assertEqual('POST', call_request.method) self.assertEqual( json.dumps(self.document).encode('utf-8'), call_request.data) self.mock_context.__enter__.assert_called_once_with() self.mock_context.__exit__.assert_called_once_with(None, None, None)
此测试遵循先创建客户机对象的
ElasticClient
要求。它不使用实际的 API 密钥,而是使用用户名和密码为Authorization
头创建一个已知值。load_eventlog()
的结果是一个类似响应的对象,可以检查它是否具有正确的值。所有这些交互都将通过模拟对象完成。我们可以使用各种断言来确认创建了正确的请求对象。测试检查请求对象的四个属性,并检查上下文是否正确使用。
-
我们还将定义一个
load_tests()
函数,将此unittest
套件与ch11_r08_load
:def load_tests(loader, standard_tests, pattern): dt = doctest.DocTestSuite(ch11_r08_load) standard_tests.addTests(dt) return standard_tests
的文档字符串中找到的任何测试示例相结合
-
最后,我们将提供运行整个套件的总体主程序。这使得以独立脚本的形式运行测试模块变得很容易:
if __name__ == "__main__": unittest.main()
这个方法结合了大量的unittest
和doctest
特性来创建一个复杂的测试用例。这些特点包括:
- 创建上下文管理器
- 使用副作用功能创建动态、有状态的测试
- 模拟复杂对象
- 使用负载测试协议组合 doctest 和 unittest 用例
我们将分别研究这些特性。
上下文管理器协议将对象包装在附加的间接层中。有关此方面的更多信息,请参阅使用上下文管理器读取和写入文件和使用多个上下文读取和写入文件方法。必须模仿的核心特性是__enter__()
和__exit__()
方法。
模拟上下文管理器的模式如下所示:
self.mock_context = Mock(
__exit__ = Mock(return_value=None),
__enter__ = Mock(
side_effect = self.create_response
# or
# return_value = some_value
),
)
上下文管理器对象有两个属性。__exit__()
将被调用一次。返回值True
将使任何异常静音。None
或False
的返回值将允许异常传播。
__enter__()
方法返回在with
语句中分配的对象。在本例中,我们使用了side_effect
属性并提供了一个函数,以便可以计算动态结果。
__enter__()
方法的一个常见替代方法是使用固定的return_value
属性,每次提供相同的管理器对象。也可以提供带有side_effect
的序列;在这种情况下,每次调用该方法时,都会返回序列中的另一个对象。
在许多情况下,测试可以使用一组静态、固定的对象。模拟响应可以在setUp()
方法中定义。但是,在某些情况下,在复杂测试的操作过程中,可能需要更改对象的状态。在这种情况下,Mock
对象的side_effect
属性可用于跟踪状态变化。
在本例中,side_effect
属性使用create_response()
方法构建动态响应。side_effect
引用的函数可以做任何事情;这可用于更新用于计算复杂响应的动态状态信息。
这里有一条细线。一个复杂的测试用例可能会引入它自己的 bug。通常,保持测试用例尽可能简单是一个好主意,以避免编写meta tests
来测试测试用例。
对于非琐碎的测试,重要的是确保测试实际上可能失败。有些测试涉及无意的重言式。可以创建一个像self.assertEqual(4, 2+2)
一样有意义的人为测试。为了确保测试实际使用了被测试的单元,当代码丢失或注入错误时,测试应该失败。
urlopen()
中的响应对象有大量的属性和方法。对于我们的单元测试,我们只需要设置其中的一些特性。
我们使用了以下方法:
return Mock(
status = 201,
getheaders = Mock(return_value=response_headers)
)
这创建了一个具有两个属性的Mock
对象:
status
属性有一个简单的数值。getheaders
属性使用具有return_value
属性的Mock
对象来创建方法函数。此方法函数返回动态response_headers
值。
response_headers
的值是具有*(键,值)*对的两个元组的序列。响应头的这种表示可以很容易地转换为字典。
对象的构建方式如下所示:
response_headers = [
('Location', self.location),
('ETag', self.database_id),
('Content-Type', 'application/json'),
]
这设置了三个标题:Location
、ETag
和Content-Type
。根据测试用例的不同,可能需要其他标题。重要的是不要让测试用例与未使用的标题混在一起。这种混乱会导致测试本身出现错误。
数据库 id 和位置基于以下计算:
hex(hash(self.mock_urlopen.call_args[0][0].data))[2:]
这将使用self.mock_urlopen.call_args
检查提供给测试用例的参数。call_args
属性的值是一个两元组,包含位置参数值和关键字参数值。位置参数也是一个元组。这意味着,call_args[0]
是位置参数,call_args[0][0]
是第一个位置参数。这将是加载到数据库的文档。
许多 Python 对象都有散列值。在这种情况下,对象应该是由json.dumps()
函数创建的字符串。此字符串的哈希值是一个大数字。该数字的十六进制值将是一个带有0x
前缀的字符串。我们将使用[2:]
片段忽略前缀。有关这方面的信息,请参见第 1 章数字、字符串和元组中的重写不可变字符串配方。
复杂模块将包括类和函数定义。模块作为一个整体需要一个描述性的 docstring。每个类和函数都需要一个 docstring。类中的每个方法也需要一个 docstring。这些将提供有关模块、类、函数和方法的基本信息。
此外,每个 docstring 可以包含一个示例。示例可通过doctest
模块进行测试。有关示例,请参见使用 docstring 进行测试配方。我们可以组合 docstring 示例测试,而不需要更复杂的单元测试。有关如何进行此操作的更多信息,请参见联合 unittest 和 DoctTest 测试配方。
unittest
模块也可用于构建集成测试。集成测试的思想是避免模拟,并在测试模式中实际使用真正的外部服务。这可能是缓慢的或昂贵的;在所有单元测试都提供了软件可能工作的信心之前,通常避免进行集成测试。
例如,我们可以使用orchestrate.io
创建两个应用程序—实际应用程序和测试应用程序。这将为我们提供两个 API 密钥。使用测试键可以将数据库重置为初始状态,而不会给实际数据的实际用户带来问题。
我们可以使用unittest
、setUpModule()
和tearDownModule()
功能进行控制。setUpModule()
功能在给定模块文件中的所有测试之前执行。这是一种将数据库设置为已知状态的简便方法。
我们还可以使用tearDownModule()
功能删除数据库。这对于删除测试创建的不需要的资源非常方便。为了调试的目的而保留资源有时会更有帮助。因此,tearDownModule()
功能可能不如setUpModule()
功能有用。
- 涉及日期或时间的测试和涉及随机性的测试配方展示了技巧。
- 第 9 章输入/输出、物理格式和逻辑布局中的使用正则表达式读取复杂格式配方,展示了如何解析复杂日志文件。在使用多个上下文读取和写入文件配方中,将复杂的日志记录写入
CSV
文件。 - 有关切碎字符串以替换部件的信息,请参阅重写不可变字符串配方。
- 可通过
doctest
模块对其元件进行测试。有关示例,请参见使用 docstring 进行测试配方。将这些测试与任何博士测试结合起来也很重要。有关如何进行此操作的更多信息,请参见联合 unittest 和 DoctTest 测试配方。