本页面仅用于内容预览,不包含动画交互效果
讲义加载中,请耐心等待……
Python 编程:从入门到实践Python 编程:从入门到实践(第三版)(第三版)Teacher Name / Email1
与《Python 编程:从入门到实践(第三版)》一书配套使用讲义中的文本及绘图采用署名-非商业性使用-相同方式共享协议CC BY-NC-SA 4.0进行许可引用的网络图片附有超链接,可用于访问来源讨论、意见、答疑、勘误、更新:https://github.com/scruel/pcc_3e_slides作者:@Scruel Tao关于本讲义关于本讲义2
1111 测试代码测试代码11.1 使用 pip 安装 pytest11.2 测试函数11.3 测试类 11.4 小结3
1111 测试代码测试代码在编写函数或类时,还可为其编写测试。通过测试:可以保证代码在面对各种输入时,都能够按要求工作。确保在程序中添加新代码时,不会破坏程序既有的行为。程序员都会犯错,良好的测试能先于用户发现问题。我们将学习并使用 pytest 库来测试代码,这是个不在标准库中三方库,所以我们首先需要安装它。4

现实中类似测试的场景有很多,比如飞机和地铁的安检、电脑和手机的测试等等

11.111.1 使用使用 pippip 安装安装 pytestpytest第三方包(third-party package指的是独立于 Python 核心的库,这些包暂时未被纳入标准库,所以需要额外安装。独立的第三方包的更新频率往往更高(由包的维护者决定)一些深受欢迎的第三方包,在稳定后可能会被纳入标准库编程中总会用到许多第三方包,但也不要盲目信任它们5

注:第三方包一般拥有自己的社区(一般是 GitHub),当你在调用它们时遇到问题后,除了通过搜索引擎搜索,还可以试试直接在社区中反馈或提问。

11.1.111.1.1 更新更新 pippipPython 提供了 pip 内置模块用于安装第三方包,这里建议将它更新到最新版,请在命令行中输入 python -m pip install --upgrade pip6

命令中的 `python -m pip` 让 Python 运行 pip 模块,`install --upgrade` 会让 pip 更新一个已安装的包,最后的 `pip` 则指定了要更新哪个第三方包,也即自己更新自己。

在输出中,`-` 后面显示的数字,就是版本号,可以看到在制作讲义时的最新版是 23.2.1,旧版本则是 23.1.2。

由于网络原因,执行过程可能需要一些时间,安装其他三方库也会如此,如果需要等待,则不妨沏一壶小茶休息一下(当然你也可以回忆回忆之前的知识。)

11.1.111.1.1 更新更新 pippip如果你已经安装了最新版本,那么输出将会是像下面这样的:更新 pip 模块是为了安全考虑,毕竟我们将使用它来和外部包打交道,频繁的更新能消除一些潜在的安全问题。7

注1:这里为了防止大家出错,命令用的是 python -m pip,实际上一般可以使用 pip 或 pip3 命令(即 pip install –upgrade pip)。如果你发现使用 pip 或 pip3 命令来执行没有问题,则可以替代使用。

注2:如果你使用的是 Linux,在安装 Python 时可能不会自动安装 pip。如果在你试图更新 pip 时出现错误消息,请参阅附录 A 提供的说明。

11.1.211.1.2 安装安装 pytestpytest接下来,我们将安装第三方包,只需要输入类似下面的命令python –m pip install --user package_name8

如果在执行这个命令时遇到麻烦,可尝试在不指定标志 --user 的情况下再次执行它。

11.1.211.1.2 安装安装 pytestpytest例如,我们可以用它来安装 pytest 这个三方库:python –m pip install --user pytest9
11.211.2 测试函数测试函数为了有东西可测试,这里编写一个简单的函数并存 name_function.py name_function.pydef get_formatted_name(first, last): """生成格式规范的姓名""" full_name = f"{first} {last}" return full_name.title()函数接受名和姓,并返回格式规范的姓名通过运行这个函数,你可以检查结果是否符合预期,这是常见人为测试手段而通过编写自动化测,可以更好地确保这个函数按预期工作10

(关于人为测试相关内容,详见原书同章节部分)

11.2.111.2.1 单元测试和测试用例单元测试和测试用例在日常的开发中,人为编写代码做简单测试的场景很常见,我们在此之前也一直都在做人为测试:即手动运行这个函数并检查其结果。人为测试看起来效率甚至还要高,毕竟自动化测试将需要编写专门的测试函数,并使用专门的测试模块,但其弊端显而易见:若想对函数不断扩展,又不希望原有功能遭到破坏,重复的手动人为测试将会令人非常烦琐随着项目逐渐庞大起来,维护成本将会呈指数级上升,并因此可能会漏掉不少人为测试项,从而可能造成一场灾难11
11.2.111.2.1 单元测试和测试用例单元测试和测试用例软件测试实际上也是一门学问,不过我们暂且无需太过深入。测试的方法有很多种,这里我们将介绍单元测试(unit test单元测试包含一组测试用例(test case,这些测试用例一道核实函数在各种情况下的行为都符合要求,良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情况的测试。全覆盖(full coverage表示涵盖了各种可能的函数使用方式,对于大型项目,要进行全覆盖测试可能很难,通常我们只需要对重要的行为编写测试,再根据实际情况考虑编写全覆盖测试。 12

注:书中对于全覆盖测试的描述是比较笼统的,测试也如同本页所说,实际上是一门学问,不妨可以在以后继续了解和学习。

test_name_function.pyfrom name_function import get_formatted_namedef test_first_last_name(): """能够正确地处理像 Janis Joplin 这样的姓名吗?""" formatted_name = get_formatted_name('janis', 'joplin') assert formatted_name == 'Janis Joplin'11.2.211.2.2 可通过的测试可通过的测试使用 pytest 进行测试,需要我们编写一个测试函数,在其中调用要测试的函数,并做出有关返回值的断言(assert)。如果断言正确,测试将通过;如果断言不正确,测试则不会通过文件名必须以 test_ 为首!13

测试文件的名称很重要,当你让 pytest 运行测试时,它将查找以 test_ 开头的文件。

test_name_function.pyfrom name_function import get_formatted_namedef test_first_last_name(): """能够正确地处理像 Janis Joplin 这样的姓名吗?""" formatted_name = get_formatted_name('janis', 'joplin') assert formatted_name == 'Janis Joplin'11.2.211.2.2 可通过的测试可通过的测试在这个测试文件中,首先导入要测试的 get_formatted_name() 函数。14

然后,定义一个测试函数 test_first_last_name() ,这个函数有几点需要注意的:

第一,测试函数也必须以 test 为首。在测试过程中,pytest 将找出并运行所有以 test 为首的函数。

第二,测试函数的名称应该比典型的函数名更长,更具描述性。

你自己不会调用测试函数,而是由 pytest 替你查找并运行它们。因此,测试函数的名称应足够长,让你在测试报告中看到它们时,能清楚地知道它们测试的是哪些行为。

test_name_function.pyfrom name_function import get_formatted_namedef test_first_last_name(): """能够正确地处理像 Janis Joplin 这样的姓名吗?""" formatted_name = get_formatted_name('janis', 'joplin') assert formatted_name == 'Janis Joplin'11.2.211.2.2 可通过的测试可通过的测试然后,定义一个测试函数,测试函数的名称应该比典型的函数名更长,且更具描述性,当然不要忘了添加说明注释。函数名必须以 test_ 为首!15

强调注意:

1. 测试函数必须以 test 为首。在测试过程中,pytest 将找出并运行所有以 test 为首的函数。

2. 测试函数的名称应足够长,让你在测试报告中看到它们时,能清楚地知道它们测试的是哪些行为。毕竟你不会自己调用测试函数,而是由 pytest 替你查找并运行它们。

test_name_function.pyfrom name_function import get_formatted_namedef test_first_last_name(): """能够正确地处理像 Janis Joplin 这样的姓名吗?""" formatted_name = get_formatted_name('janis', 'joplin') assert formatted_name == 'Janis Joplin'11.2.211.2.2 可通过的测试可通过的测试函数名必须以 test_ 为首!接下来,和人为测试一样,我们调用被测试的函数,通过传入任意合理的参数(此处即名字字符串),可以获取到函数的返回值。16
test_name_function.pyfrom name_function import get_formatted_namedef test_first_last_name(): """能够正确地处理像 Janis Joplin 这样的姓名吗?""" formatted_name = get_formatted_name('janis', 'joplin') assert formatted_name == 'Janis Joplin'11.2.211.2.2 可通过的测试可通过的测试函数名必须以 test_ 为首!最后,做出一个开头提出的断言,对结果值和预期值进行比较,Python 将为我们检查结果。17
11.2.211.2.2 可通过的测试可通过的测试断言(assertion就是声称满足特定的条件:这里声称 formatted_name 的值为 'Janis Joplin'如果断言不成立(条件测试的结果为 False),Python 将会给出一个错误,我们能在测试报告中看到这个错误。assert formatted_name == 'Janis Joplin'结果值预期值18

结果值:一般来自于被测试的函数的返回值。

预期值:可以是根据北侧函数代码推出的值,也可以是通过人为测试得到的符合预期的函数结果。

11.2.311.2.3 运行测试运行测试若是直接运行文件 test_name_function.py,将不会有任何输出,因为我们没有在文件中调用这个测试函数。为了进行测试,我们应该让 pytest 替我们运行这个测试文件。19
11.2.311.2.3 运行测试运行测试首先打开一个终端窗口,并切换到测试文件所在的文件夹。如果用的是 VS Code,则可以使用该编辑器内嵌的终端窗口:20

如果忘了如何在终端窗口中切换到正确的文件夹,请参阅 1.5 节,简单来说:

在 windows 中,可以通过 `pushd <文件夹路径>` 来切换到指定的路径,例如 pushd d:/code

在 linux 中,则使用 cd 命令(windows 事实上也带有 cd 命令,但还是建议你使用 pushd 命令)

注:如果你使用类似与 Pycharm 等软件时(IDE,集成开发环境),在打开文件后,可能就能看到测试函数旁有运行按钮,点击它将自动为你执行测试;不过我们用的是 VS Code,其本质是一款编辑器,额外的功能都是由插件提供的,对于这一章,我们并不会推荐任何相关插件。不过在初学时多熟悉一些终端命令,总不会是坏事。

11.2.311.2.3 运行测试运行测试在终端窗口中执行命令 python -m pytest,你将看到如下输出:21

下面来尝试解读这些输出。首先,我们看到了一些有关运行测试的系统的信息。

我是在 Linux 系统中运行该测试的,因此你看到的输出可能与这里显示的不同(书中是在 MacOS 中运行的),这一行后面的输出,指出了用来运行该测试的 Python、pytest 和其他相关包的版本。

注1:和之前的 pip 命令一样,为了防止大家出错,此处的命令用的是完整的 python -m pytest,而不是简单直接的 pytest,如果你发现 pytest 执行没有问题,则可以替代使用。

注2:终端是否能显示彩色取决于终端本身,你所用的终端可能不会像图片一样带有丰富的颜色。

11.2.311.2.3 运行测试运行测试在终端窗口中执行命令 python -m pytest,你将看到如下输出:22

接下来,可以看到该测试是从哪个目录运行的,这里是 /home/scruel/code。

如你所见,pytest 找到了一个测试,并指出了运行的是哪个测试文件。

文件名后面的点号表明有一个测试通过了,而 100% 指出运行了所有的测试。

在可能有数百乃至数千个测试的大型项目中,点号和完成百分比有助于监控测试的运行进度。

11.2.311.2.3 运行测试运行测试在终端窗口中执行命令 python -m pytest,你将看到如下输出:23

最后一行指出有一个测试通过了,运行该测试花费的时间不到 0.01 秒。

上述输出表明,在给定包含名和姓的姓名时,get_formatted_name() 函数总是能正确地处理它。

修改 get_formatted_name() 后,可再次运行测试。如果它通过了,就表明在给定 Janis Joplin 这样的姓名时,这个函数依然能够正确地处理。

11.2.411.2.4 未通过的测试未通过的测试测试未通过时的结果是什么样的呢?我们来修改 get_formatted_name(),使其能够处理中间名,但故意让这个函数无法正确地处理像 Janis Joplin 这样只有名和姓的姓名。下面是函数的新版本,它要求通过一个实参指定中间名:name_function.pydef get_formatted_name(first, middle, last): """生成格式规范的姓名""" full_name = f"{first} {middle} {last}" return full_name.title()24

这个版本应该能够正确地处理包含中间名的姓名,但对其进行测试时,我们发现它不再能正确地处理只有名和姓的姓名了。

11.2.411.2.4 未通过的测试未通过的测试这次运行 pytest 时,输出如下:25

这里的信息很多,因为在测试未通过时,需要你知道的事情可能有很多。

首先,输出中有一个字母 F,表明有一个测试未通过。

11.2.411.2.4 未通过的测试未通过的测试这次运行 pytest 时,输出如下:26

然后是 FAILURES 部分,这是关注的焦点,因为在运行测试时,通常应该关注未通过的测试。

在这里,pytest 指出未通过的测试函数是 test_first_last_name()

右尖括号(>)为首的那行,指出了导致测试未能通过的代码

以 E 为首的下一行中,指出了导致测试未通过的具体错误:缺少必不可少的位置实参 'last‘,导致 TypeError。

空一行后接下来的一行,指出了导致测试未通过的文件名,以及涉及的代码行数。

11.2.411.2.4 未通过的测试未通过的测试这次运行 pytest 时,输出如下:27

在末尾的简短小结中,再次列出了最重要的信息。

这样,即使你运行了很多测试,也可快速获悉哪些测试未通过以及测试未通过的原因。

11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办如果检查的条件没错,那么测试通过就意味着函数的行为是对的测试未通过意味着你编写的新代码有错因此,在测试未通过时,不要修改测试本身。因为如果你这样做,即便能让测试通过,像测试那样调用函数的代码也将突然崩溃。相反,应修复导致测试不能通过的被测函数的代码:检查刚刚对函数所做的修改,找出这些修改是如何导致函数行为不符合预期的。28
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办在这个示例中,get_formatted_name() 以前只需要名和姓这两个实参,但现在要求提供名、中间名和姓,而且新增的中间名参数是必不可少的。这一修改,导致了函数的行为与原来不同name_function.pydef get_formatted_name(first, middle, last): """生成格式规范的姓名""" full_name = f"{first} {middle} {last}" return full_name.title()29
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办就这里而言,最佳的选择是让中间名变为可选的。这样,不仅在使用类似于 Janis Joplin 的姓名进行测试时可以通过,而且这个函数还能接受中间名。name_function.pydef get_formatted_name(first, middle, last): """生成格式规范的姓名""" full_name = f"{first} {middle} {last}" return full_name.title()30
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办要将中间名设置为可选的,可在函数定义中将形参 middle 移到形参列表末尾,并将其默认值指定为一个空字符串。31
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办要将中间名设置为可选的,可在函数定义中将形参 middle 移到形参列表末尾,并将其默认值指定为一个空字符串。还需要添加一 if 测试,以便根据是否提供了中间名相应地创建姓名:name_function.pydef get_formatted_name(first, middle, last): """生成格式规范的姓名""" full_name = f"{first} {middle} {last}" return full_name.title()32

如果向这个函数传递了中间名,姓名将包含名、中间名和姓,否则将只包含名和姓。

现在,对于这两种不同的姓名,这个函数应该都能够正确地处理了。

11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办要将中间名设置为可选的,可在函数定义中将形参 middle 移到形参列表末尾,并将其默认值指定为一个空字符串。还需要添加一 if 测试,以便根据是否提供了中间名相应地创建姓名:name_function.pydef get_formatted_name(first, last, middle=''): """生成格式规范的姓名""" if middle: full_name = f"{first} {middle} {last}" else: full_name = f"{first} {last}" return full_name.title()33

如果向这个函数传递了中间名,姓名将包含名、中间名和姓,否则将只包含名和姓。

现在,对于这两种不同的姓名,这个函数应该都能够正确地处理了。

11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办如果向这个函数传递了中间名,姓名将包含名、中间名和姓,否则将只包含名和姓。现在,对于这两种不同的姓名,这个函数应该都能够正确地处理了。name_function.pydef get_formatted_name(first, last, middle=''): """生成格式规范的姓名""" if middle: full_name = f"{first} {middle} {last}" else: full_name = f"{first} {last}" return full_name.title()34
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办为了确定这个函数依然能够正确地处理像 Janis Joplin 这样的姓名,再次运行测试:测试通过了,太好了!35
11.2.511.2.5 在测试未通过时怎么办在测试未通过时怎么办要注意到,我们没有修改测试函数的代码,修改的是被测函数中导致结果不符合预期的新代码;我们也不需要重新编写测试代码。因为未通过的测试帮我们识别出了新代码是如何破坏函数原有行为的,所以函数的修复工作变得更容易了。36
11.2.611.2.6 添加新测试添加新测试确定 get_formatted_name() 又能正确地处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,在文件 test_name_function.py 中添加一个测试函数:test_name_function.py--snip--def test_first_last_middle_name(): """能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名吗?""" formatted_name = get_formatted_name( 'wolfgang', 'mozart', 'amadeus') assert formatted_name == 'Wolfgang Amadeus Mozart'37

我们将这个新函数命名为 test_first_last_middle_name(),再次强调,函数名必须以 test 为首,这样该函数才会在我们运行 pytest 时被自动运行 。

函数名清楚地指出了它测试的是 get_formatted_name() 的哪个行为,如果该测试未通过,我们就能马上知道受影响的是哪种类型的姓名。

11.2.611.2.6 添加新测试添加新测试为测试修改后的 get_formatted_name() 函数,与之前一样的是,我们先使用名、姓和中间名调用它test_name_function.py--snip--def test_first_last_middle_name(): """能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名吗?""" formatted_name = get_formatted_name( 'wolfgang', 'mozart', 'amadeus') assert formatted_name == 'Wolfgang Amadeus Mozart'38
11.2.611.2.6 添加新测试添加新测试为测试修改后的 get_formatted_name() 函数,与之前一样的是,我们先使用名、姓和中间名调用它,再断言预期输出结果。test_name_function.py--snip--def test_first_last_middle_name(): """能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名吗?""" formatted_name = get_formatted_name( 'wolfgang', 'mozart', 'amadeus') assert formatted_name == 'Wolfgang Amadeus Mozart'39
11.2.611.2.6 添加新测试添加新测试再次运行 pytest,可以看到有两个测试,且都通过了:40

两个点号表明有两个通过的测试,最后一行输出文字也清楚地指出了这一点。

现在我们知道,这个函数又能正确地处理像 Janis Joplin 这样的姓名了,而且能确定它还能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名。

11.311.3 测试类测试类 之前所讲的都是针对单个函数的测试,下面来编写针对类的测试。很多程序会用到类,因此证明类能够正确地工作十分必要。如果针对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的行为。到目前为止,我们只介绍了一种断言:声称一个字符串变量取预期的值。实际上在编写测试时,可做出任何可表示为条件语句的断言如果该条件确实成立,你对程序行为的假设就得到了确认,可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,测试就不能通过,让你知道代码存在需要解决的问题。41

注:原书中的“确信其中没有错误”是相对来说的,毕竟我们编写的测试代码也是有可能有错误的,或者可能会是不全面的。

11.3.111.3.1 各种断言各种断言现在可以了解一些可在测试中包含的其他有用的断言了:42

这里只是列出了一部分内容,大家可以回顾一下之前第 5 章中提及的条件测试等相关内容,事实上断言后面放的,就是条件测试。

11.3.211.3.2 一个要测试的类一个要测试的类类的测试与函数的测试相似,所做的大部分工作是测试类中方法的行为,不过二者还是存在一些不同之处。下面编写一个要测试的类,其用于协助管理匿名调查:survey.pyclass AnonymousSurvey: """收集匿名调查问卷的答案""" def __init__(self, question): self.question = question self.responses = [] def store_response(self, new_response): self.responses.append(new_response) def show_results(self): print("Survey results:") for response in self.responses: print(f"- {response}")43

这个类首先存储一个调查问题,并创建了一个空列表,用于存储答案。

这个类包含在答案列表中添加新答案的方法,以及将存储在列表中的答案打印出来的方法。要创建这个类的实例,只需提供一个问题即可。有了表示调查的实例,就可以使用 show_question() 来显示其中的问题,使用 store_response() 来存储答案,并使用 show_results() 来显示调查结果了。

11.3.211.3.2 一个要测试的类一个要测试的类这个类首先存储一个调查问题,并创建了一个空列表,用于存储答案。survey.pyclass AnonymousSurvey: """收集匿名调查问卷的答案""" def __init__(self, question): self.question = question self.responses = [] def store_response(self, new_response): self.responses.append(new_response) def show_results(self): print("Survey results:") for response in self.responses: print(f"- {response}")44

注:限于版面,这里对原书中的类进行了简化,去掉了 show_question 这个方法,这一修改不会影响测试的结果。

11.3.211.3.2 一个要测试的类一个要测试的类这个类首先存储一个调查问题,并创建了一个空列表,用于存储答案。同时包含在答案列表中添加新答案的方法,以及将存储在列表中的答案(response)打印出来的方法。survey.pyclass AnonymousSurvey: """收集匿名调查问卷的答案""" def __init__(self, question): self.question = question self.responses = [] def store_response(self, new_response): self.responses.append(new_response) def show_results(self): print("Survey results:") for response in self.responses: print(f"- {response}")45

要创建这个类的实例,只需提供一个问题即可。

有了表示调查的实例,就可以使用 store_response() 来存储答案,并使用 show_results() 来显示调查结果了。

11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey 下面来编写一个测试,对 AnonymousSurvey 类的行为的一个方面进行验证——如果用户在面对调查问题时只提供一个答案,这个答案也能被妥善地存储:test_survey.pyfrom survey import AnonymousSurveydef test_store_single_response(): """测试单个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) language_survey.store_response('English') assert 'English' in language_survey.responses46
11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey test_survey.pyfrom survey import AnonymousSurveydef test_store_single_response(): """测试单个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) language_survey.store_response('English') assert 'English' in language_survey.responses首先,导入要测试的 AnonymousSurvey 类。测试函数验将会验证:调查问题的单个答案被存储后,它会被包含在调查结果列表中。47

对于这个测试函数,一个不错的描述性名称是 test_store_single_response()。

如果这个测试未通过,我们就能通过测试小结中的函数名得知,在存储单个调查答案方面存在问题。

11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey test_survey.pyfrom survey import AnonymousSurveydef test_store_single_response(): """测试单个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) language_survey.store_response('English') assert 'English' in language_survey.responses要测试类的行为,需要创建其实例,为此我们提供了一个问题,然后使用 store_response 方法存储单个文本答案 English48
11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey test_survey.pyfrom survey import AnonymousSurveydef test_store_single_response(): """测试单个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) language_survey.store_response('English') assert 'English' in language_survey.responses接下来,通过断言 English 在列表 language_survey.responses 之中,来核实这个答案被妥善地存储了。49
11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey 下面运行为调查类编写的测试,命令中指定了要测试文件名:如果在执行命令 pytest 时没有指定任何参数,pytest 运行它在当前目录中找到的所有测试。这里为了专注于一个测试文件,我们将特定的测试文件的名称作为参数,传递给了 pytest50
11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey 可以看到结果显示测试通过了:51
test_survey.pyfrom survey import AnonymousSurvey--snip--def test_store_three_response(): """测试三个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) responses = ['English', 'Spanish', 'Mandarin'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey 只能收集一个答案的调查用途不大。下面来核实,当用户提供三个答案时,它们都将被妥善地存储。为此,再添加一个测试函数:52

我们将这个新函数命名为 test_store_three_responses(),并像 test_store_single_response() 一样,在其中创建一个调查对象。

然后定义一个包含三个不同答案的列表,再对其中的每个答案都调用 store_response()。

存储这些答案后,使用一个循环来断言每个答案都包含在 language_survey.responses 中。

11.3.311.3.3 测试测试 AnonymousSurveyAnonymousSurvey 再次运行这个测试文件,两个测试(针对单个答案的测试和针对三个答案的测试)都通过了:53
11.3.411.3.4 使用夹具使用夹具之前的测试中,我们在每个测试函数中都创建了一个类的实例,码也都是重复的,这在包含数十乃至数百个测试的项目中是个大问题:每次的小修改都需要同步到各处,需要牵一发而动全身DRYDRY54

这里则引出了一个代码编写过程中的简单原则,即 DRY

11.3.411.3.4 使用夹具使用夹具之前的测试中,我们在每个测试函数中都创建了一个类的实例,码也都是重复的,这在包含数十乃至数百个测试的项目中是个大问题:每次的小修改都需要同步到各处,需要牵一发而动全身Don’tDon’t RepeatRepeat YourselfYourself55

直译为:不要重复你自己,意思是不要通过复制粘贴,重复编写一样的代码,避免以后修改时要“牵一发而动全身”。

11.3.411.3.4 使用夹具使用夹具夹具(fixture可帮助我们搭建初始的测试环境,比如创建一些供多个测试使用的公用资源。装饰器(decorator是放在函数定义前的指令。Python 将应用该指令于函数,以改变函数的行为。这听起来很复杂,但是不用担心:即便没有学习如何编写装饰器,也可使用其他人编写好的装饰器,目前只要学会使用即可。若想在 pytest 中使用夹具来简化代码,可编写一个使用装饰器 @pytest.fixture 装饰的函数。56

注1:夹具这个词有点怪,不过为了迎合原书就保留了,实际上个人感觉用“固定装置”的直译更好。

注2:装饰器本质上其实是一个特殊的函数或类,我们暂时不需要学习如何编写它,目前只需要学会如何使用即可。

11.3.411.3.4 使用夹具使用夹具下面使用夹具创建一个 AnonymousSurvey 实例,这个实例可以 test_survey.py 中的两个测试函数中被使用:test_survey.pyimport pytestfrom survey import AnonymousSurvey@pytest.fixturedef language_survey(): """一个可供所有测试函数使用的 AnonymousSurvey 实例""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) return language_survey--snip--57
11.3.411.3.4 使用夹具使用夹具import pytestfrom survey import AnonymousSurvey@pytest.fixturedef language_survey(): """一个可供所有测试函数使用的 AnonymousSurvey 实例""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) return language_survey首先需要导入 pytest,因为夹具装饰器的功能是其提供的58
11.3.411.3.4 使用夹具使用夹具import pytestfrom survey import AnonymousSurvey@pytest.fixturedef language_survey(): """一个可供所有测试函数使用的 AnonymousSurvey 实例""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) return language_survey为了使用夹具,我们需要编写一个函数,并将夹具装饰器 @pytest.fixture 应用至函数(即放置在函数的上一行)59
11.3.411.3.4 使用夹具使用夹具import pytestfrom survey import AnonymousSurvey@pytest.fixturedef language_survey(): """一个可供所有测试函数使用的 AnonymousSurvey 实例""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) return language_survey这个函数的函数体代码,和之前创建实例对象一样,函数最后会返回所创建的对象。60
11.3.411.3.4 使用夹具使用夹具使用夹具之前,我们回顾一下之前用于测试单个答案的函数:test_survey.py--snip--def test_store_single_response(): """测试单个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) language_survey.store_response('English') assert 'English' in language_survey.responses--snip--61
11.3.411.3.4 使用夹具使用夹具下面则是经过简化的测试函数:test_survey.py--snip--@pytest.fixturedef language_survey(): --snip--def test_store_single_response(language_survey): """测试单个答案会被妥善地存储""" language_survey.store_response('English') assert 'English' in language_survey.responses--snip--62

请注意,测试函数的定义有变化:增加了一个名为 language_survey 的形参。

我们删除了测试函数中的两行代码,没有新增代码,删除的分别是:

定义问题字符串的代码行

创建 AnonymousSurvey 对象的代码行

11.3.411.3.4 使用夹具使用夹具下面则是经过简化的测试函数:test_survey.py--snip--@pytest.fixturedef (): --snip--def test_store_single_response( ): """测试单个答案会被妥善地存储""" language_survey.store_response('English') assert 'English' in language_survey.responses--snip--language_surveylanguage_survey63

当测试函数的一个形参与应用了装饰器 @pytest.fixture 的函数同名时,将自动运行这个同名的夹具,并将夹具返回的值传递给测试函数。

在这个示例中,language_survey() 函数向 test_store_single_response() 提供了一个 language_survey 实例。

11.3.411.3.4 使用夹具使用夹具同样地,我们可以简化另一个测试函数,同样回顾:test_survey.py--snip--def test_store_three_response(): """测试三个答案会被妥善地存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) responses = ['English', 'Spanish', 'Mandarin'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses64
11.3.411.3.4 使用夹具使用夹具简化后的函数代码如下:test_survey.py--snip--@pytest.fixturedef language_survey(): --snip--def test_store_three_response(language_survey): """测试三个答案会被妥善地存储""" responses = ['English', 'Spanish', 'Mandarin'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses65
11.3.411.3.4 使用夹具使用夹具总结一下,更新后的两个函数如下:test_survey.py--snip--def test_store_single_response(language_survey): """测试单个答案会被妥善地存储""" language_survey.store_response('English') assert 'English' in language_survey.responsesdef test_store_three_response(language_survey): """测试三个答案会被妥善地存储""" responses = ['English', 'Spanish', 'Mandarin'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses66

两个测试函数的定义都变了,且两个测试函数都没有新增代码,而且都删除了两行代码:定义问题的代码行, 以及创建 AnonymousSurvey 对象的代码行。

11.3.411.3.4 使用夹具使用夹具再次运行这个测试文件,可以看到定义的两个测试都能通过:67

如果要扩展 AnonymousSurvey,使其允许 每个用户输入多个答案,这些测试将很有用:修改代码以接受多个答案后,你可运行这些测试, 确认存储单个答案或一系列答案的行为未受影响。

11.3.411.3.4 使用夹具使用夹具之前代码的结构看起来很复杂,包含一些较为抽象的代码。你并非一定要马上使用夹具,即使编写包含大量重复代码的测试也胜过根本不编写测试。你只需知道:如果编写的测试代码测试包含大量重复的代码,有一种已得到验证的方式可用来消除重复的代码另外,对于简单的测试,使用夹具并不一定能让代码更简洁、更容易理解;但在项目包含大量测试需要使用很多行代码创建供多个测试使用的资源的情况下,使用夹具可极大地改善测试代码的质量,使得测试代码更简洁,编写和维护也将更容易。68
聊聊测试聊聊测试测试是很多初学者并不熟悉的主题,并非必须为自己尝试的所有项目编写测试,但在参与工作量较大的项目时,应该对自己编写的函数和类的重要行为进行测试测试能让你确信自己所做的工作不会破坏项目的其他部分。测试能在你不小心破坏既有功能时给你打报告,从而使你能轻松地定位和修复问题。比起等到不满意的用户报告问题后再采取措施,在测试未通过时就着手解决问题,要容易得多的多。69

推荐阅读:《人月神话》和《人件》等软件工程相关书籍,你或许能更好地理解测试的作用。

聊聊测试:团队合作聊聊测试:团队合作编写测试有利于团队合作,能让其他人更放心地参与进来:如果你在项目中纳入了测试,其他程序员不仅能够更得心应手地使用你编写的代码,也更愿意与你合作开发项目。要给其他程序员开发的项目贡献代码,就必须证明你编写的代码通过了既有的测试,而且通常需要为你添加的新行为编写测试。建议通过多多开展测试来熟悉代码测试过程。对于自己编写的函数和类,请编写针对其重要行为的测试。在较为早期的项目中,不必以编写以全覆盖测试为目标的测试,除非有充分的理由。70
11.411.4 小结小结学习了如何使用 pytest 模块中的工具来为函数和类编写测试。不仅学习了如何编写测试函数,以核实函数和类的行为符合预期,而且学习了如何使用夹具来高效地创建可在测试文件中的多个测试函数中使用的资源。接下来,我们将带着你通过编写项目,让你将基础知识学以致用!恭喜你学完了基础篇的所有内容!记得稍事休息,奖励一下自己!71
课后拓展课后拓展完成书中的动手试一试好好放松一下可选拓展学习如何自定义断言的消息,以及 pytest 中的断言函数了解装饰器(decorator)是怎么定义的了解更多编写整洁高效代码的基本原则了解软件测试的三大类及白盒测试中的覆盖率概念了解面向测试驱动开发(TDD)的理念了解什么是设计模式,想想 Python 中如何实现72