Python 单元测试自动化教程(一)
一、Python 简介
我希望你已经浏览了介绍部分。如果你还没有,那么我推荐你读一读,因为它会帮助你理解这本书的内容和哲学。
让我们从学习 Python 的历史和背景开始这次冒险之旅。
我个人觉得 Python 很神奇,已经被它迷住了。Python 是一种简单而强大的编程语言。当使用 Python 时,很容易关注给定问题的解决方案的实现,因为程序员不必担心编程语言的语法。
Python 的历史
Python 诞生于 20 世纪 80 年代末。吉多·范·罗苏姆于 1989 年底在荷兰的 Centrum Wiskunde & Informatica(国家数学和计算机科学研究所)开始实施这一计划。Python 是 ABC 编程语言的继承者,ABC 编程语言本身就是受 SETL 的启发。1991 年 2 月,Van Rossum 向alt.sources新闻组发布了 Python 代码。Python 这个名字的灵感来自电视节目“巨蟒剧团的飞行马戏团”,因为 Van Rossum 是巨蟒剧团的忠实粉丝。
Van Rossum 是 Python 的主要作者。他在指导 Python 的发展和演变中发挥了核心作用。他拥有 Python 的终身仁慈独裁者的称号。2018 年,他卸任了那个角色。截至撰写本版时,他在微软工作。
Python 的核心哲学,被称为 Python 的*禅,*在 PEP-20 中有解释,可以在 https://www.python.org/dev/peps/pep-0020 找到。
它是 20 个软件原则的集合,如下所示:
-
漂亮总比难看好。
-
显性比隐性好。
-
简单比复杂好。
-
复杂总比复杂好。
-
平面比嵌套好。
-
疏比密好。
-
可读性很重要。
-
特例不足以特殊到打破规则。
-
实用性胜过纯粹性。
-
错误永远不会无声无息地过去。
-
除非明确沉默。
-
面对暧昧,拒绝猜测的诱惑。
-
应该有一种——最好只有一种——显而易见的方法来做这件事。
-
尽管这种方式一开始可能并不明显,除非你是荷兰人。
-
现在总比没有好。
-
虽然从来没有比现在更好。
-
如果实现很难解释,这是一个坏主意。
-
如果实现很容易解释,这可能是一个好主意。
-
名称空间是一个非常棒的想法——让我们多做一些吧!
Python 的特性
下面几节讨论 Python 的一些特性,这些特性在编程社区中已经变得流行和受欢迎。
简单的
Python 是一种简单的极简主义语言。阅读一个写得很好的 Python 程序,会让你觉得好像在读英文文本。
简单易学
由于其简单和类似英语的语法,Python 对于初学者来说非常容易学习。
这就是为什么现在它作为第一编程语言被教授给学习编程入门和编程 101 课程的高中生和大学生的主要原因。新一代的程序员正在学习 Python 作为他们的第一门编程语言。
易于阅读
与其他高级编程语言不同,Python 没有提供太多混淆代码和使其不可读的规定。与用其他编程语言编写的代码相比,Python 代码的类似英语的结构更容易阅读。与 C 和 C++等其他高级语言相比,这使得它更容易理解和学习。
易于维护
由于 Python 代码易于阅读、理解和学习,任何维护代码的人都可以在相当短的时间内熟悉其代码库。我可以从维护和增强由 bash 和 Python 2 组合编写的大型遗留代码库的个人经历中证明这一点。
开放源码
Python 是一个开源项目。这意味着它的源代码是免费的。您可以根据自己的需要对它进行修改,并在应用中使用原始的和修改过的代码。
高级语言
在编写 Python 程序时,您不必管理低级别的细节,如内存管理、CPU 计时和调度过程。所有这些任务都由 Python 解释器管理。你可以直接用易于理解的、类似英语的语法写代码。
轻便的
Python 已经移植到很多平台。所有 Python 程序都可以在这些平台上运行,不需要任何修改,如果你足够小心避免任何系统相关的特性。您可以在 GNU/Linux、Windows、Android、FreeBSD、macOS、iOS、Solaris、OS/2、Amiga、AROS、AS/400、BeOS、OS/390、z/OS、Palm OS、QNX、VMS、Psion、Acorn、RISC OS、VxWorks、PlayStation、Sharp Zaurus、Windows CE 和 PocketPC 上使用 Python。
解释
Python 是一种直译语言。用 C、C++和 Java 等高级编程语言编写的程序首先被编译。这意味着它们首先被转换成中间格式。当您运行程序时,这个中间格式由链接器/加载器从辅助存储器(即硬盘)加载到内存(ram)中。所以,C、C++和 Java 有独立的编译器和连接器/加载器。Python 就不是这样了。Python 直接从源代码运行程序。您不必担心编译和链接到适当的库。这使得 Python 程序真正具有可移植性,因为您可以将程序从一台计算机复制到另一台计算机,只要在目标计算机上安装了必要的库,程序就可以正常运行。
面向对象
Python 支持面向对象的编程范例。在面向对象的编程语言中,程序是围绕结合数据和相关功能的对象构建的。Python 是一种非常简单但功能强大的面向对象编程语言。
可扩张的
Python 的一个特性就是可以从 Python 程序中调用 C 和 C++例程。如果希望应用的核心功能运行得更快,可以用 C/C++编写那部分代码,在 Python 程序中调用(C/C++程序一般比 Python 运行得快)。
丰富的图书馆
Python 预装了一个广泛的标准库。标准库具有现代编程语言的所有基本特征。它提供了数据库、单元测试(我们将在本书中探讨)、正则表达式、多线程、网络编程、计算机图形、图像处理、GUI 和其他工具。这是 Python 的内置电池理念的一部分。
除了标准库,Python 还有一个庞大且不断增长的第三方库。这些库的列表可以在 Python 包索引( https://pypi.org/ )中找到。在本书中,我们将探索一些用于测试自动化的库,如unittest、nose、nose2、pytest和selenium。我还参与了科学计算和计算机视觉库的工作并撰写了大量文章,比如numpy、scipy、matplotlib、pillow、scikit-image和 OpenCV。
粗野的
Python 通过其处理错误的能力来提供健壮性。遇到的错误的完整堆栈跟踪是可用的,这使得程序员的生活更容易忍受。运行时错误被称为异常。允许处理这些错误的特性被称为异常处理机制。
快速原型
Python 作为快速成型工具。正如您所读到的,Python 拥有丰富的库并且易于学习,因此许多软件架构师越来越多地使用它作为工具,在很短的时间内将他们的想法快速原型化为工作模型。
内存管理
在汇编语言和像 C 和 C++这样的编程语言中,内存管理是程序员的责任。这是手头任务之外的。这给程序员造成了不必要的负担。在 Python 中,Python 解释器处理内存管理。这有助于程序员避开内存问题,专注于手头的任务。
强大的
Python 拥有现代编程语言的一切。它用于计算机视觉、超级计算、药物发现、科学计算、模拟和生物信息学等应用。全世界数百万程序员使用 Python。许多大型组织,如 NASA、Google、SpaceX 和 Cisco,都在他们的应用和基础设施中使用 Python。
社区支持
我发现这是 Python 最吸引人的特性。正如您所读到的,Python 是开源的,在全世界有一个由近百万程序员组成的社区(可能更多,因为今天的高中生正在学习 Python)。这意味着互联网上有很多论坛支持遇到障碍的程序员。我提出的与 Python 相关的问题没有一个是没有答案的。
蟒蛇 3
Python 3 发布于 2008 年。Python 开发团队决定去掉 Python 语言的一些冗余特性,简化它的一些特性,纠正一些设计缺陷,并添加一些急需的特性。
人们决定,主要修订号是有保证的,并且最终发布的版本不会向后兼容。Python 2.x 和 3.x 应该并行共存,以便程序员社区有足够的时间将他们的代码和第三方库从 2.x 迁移到 3.x. Python 2.x 代码在许多情况下无法在 Python 3 上运行,因为 2.x 和 3.x 之间存在显著差异。
Python 2 和 Python 3 的区别
以下是 Python 2 和 Python 3 之间最显著的区别。让我们简单地看一下它们:
-
print()功能这可能是 Python 2 和 Python 3 之间最显著的区别。Python 2 的
print语句在 Python 3 中被替换为print()函数。 -
整数除法
在 Python 3 中,为了数学正确性,整数除法的性质已经改变。在 Python 2 中,两个整数操作数相除的结果是一个整数。但是,在 Python 3 中,它是一个浮点值。
-
省略
xrange()在 Python 2 中,为了创建可迭代对象,使用了
xrange()函数。在 Python 3 中,range()的实现很像xrange()。因此,不再需要单独的xrange()。在 Python 3 中使用xrange()会引发一个nameError。 -
引发异常
在 Python 3 中,必须将异常参数(如果有的话)括在括号中,而在 Python 2 中,这是可选的。
-
处理异常
在 Python 3 中,在处理异常时,需要在参数前使用
as关键字来处理参数。在 Python 2 中,不需要。 -
新样式类别
Python 2 支持旧的和新的样式类,而 Python 3 只支持新的样式类。默认情况下,Python 3 中创建的所有类都使用新的样式类。
-
Python 3 的新特性
The following new features of Python 3 have not been backported to Python 2:
-
默认情况下,字符串是 Unicode 的
-
清除 Unicode/字节分隔
-
异常链接
-
函数注释
-
仅关键字参数的语法
-
扩展元组解包
-
非局部变量声明
-
从这个列表中,你会经常在本书的代码示例中使用到print()、新型类和异常。
为什么使用 Python 3
从前面的列表中,您将会在本书的代码示例中频繁使用新型类和异常。
Python 的 wiki 页面( https://wiki.python.org/moin/Python2orPython3 )是这样说的:
对于任何新的开发都强烈推荐 Python 3。
新一代程序员将 Python 3 作为他们的第一门编程语言。当他们熟悉 Python 编程的概念和哲学时,他们会逐渐了解 Python 2,这样他们也可以使用遗留代码库。许多组织已经开始将代码库从 Python 2 迁移到 Python 3。Python 中的所有新项目都大量使用 Python 3。在撰写本书的这个版本时,Python 2 几乎已经寿终正寝了。大多数组织正在将他们的遗留代码库从 Python 2 迁移到 Python 3。随着时间的推移,Python 2 中的代码越来越少,要么被放弃,要么被转换到 Python 3。将 Python 2 代码转换成 Python 3 是一个漫长而繁琐的过程。许多组织都是根据需要来做的。大多数组织遵循的一般经验法则是,如果代码有效,他们就不碰它。然而,正如我已经说过的,所有涉及 Python 的新项目都是从 Python 3 开始的。展望未来,将遗留的 Python 2 代码库转换为 Python 3 将为专业人员带来技术上的挑战和经济上的巨大机遇。
我个人认为这些是使用 Python 3 的很好的理由。
安装 Python 3
本节讨论如何在各种常用的计算机操作系统上安装 Python 3。
在 Linux 上安装
Linux 有许多流行的发行版。Python 3 预装在许多流行的发行版中。
安装在 Debian、Ubuntu 和衍生工具上
Python 3 解释器预装在 Debian、Ubuntu 及其衍生产品的最新版本中。
在 Fedora 和 CentOS 上安装
Python 3 解释器预装在 Fedora 和 CentOS 的最新版本中。
Note
在大多数最新的 Linux 发行版上,默认情况下会安装两个版本的 Python (Python 2 和 Python 3)。Python 2 的解释器是一个名为python的二进制可执行文件,Python 3 的解释器是另一个名为python3的二进制可执行文件。您可以使用python3 --V或python3 --version来检查安装在您的 Linux 计算机上的 Python 3 解释器的版本。同样,你可以使用which python3命令来确定解释器在磁盘上的位置。
在 macOS X 上安装
在 macOS X 上,Python 2 解释器是默认安装的,可以使用python命令从终端调用。如果你想用 Python 3,你必须安装它。确保计算机连接到互联网,并在终端中运行brew install python3命令。这将安装 Python 3。它还将安装其他工具,如pip、setuptools和wheel。
一旦安装完成,进入终端并键入python3 --V或python3 --version来检查安装的 Python 3 解释器的版本。
在 Windows 上安装
在 Windows 操作系统中,Python 3 的安装需要更多的努力。Python 2 或 Python 3 没有预装在 Windows 计算机上。为了安装它,你必须在 https://www.python.org/downloads 访问 Python 网站的下载部分,如图 1-1 所示。
图 1-1
网站上的 Python 下载部分
选择 Python 3.5.2。(如果本书出版后有新的 Python 稳定版本,数字 5 和 2 可能会改变。)这将下载 Python 3 的安装文件。下载后打开安装文件。点击图 1-2 所示对话框中的运行按钮。
图 1-2
打开文件-安全警告对话框
Note
根据设置,您可能需要管理员权限才能在 Windows 计算机上安装 Python 3(或任何其他程序)。如果您处于组织环境中,请向您的系统管理团队咨询此信息。
如果您使用的是 Windows 7,根据您计算机的更新状态,您可能会遇到如图 1-3 所示的消息框。
图 1-3
Windows 7 的安装失败消息
通过安装任何 Windows 更新来更新操作系统,然后重新运行安装文件。成功后会出现图 1-4 中的窗口。
图 1-4
Python 安装窗口
选中将 Python 3.x 添加到路径复选框。这将确保 Python 被添加到PATH系统变量中,并且您将能够在安装后从 Windows 的命令提示符(cmd)访问 Python。单击“立即安装”按钮,继续安装向导。安装完成后,将显示一条成功消息。
运行 Python 程序和 Python 模式
现在,您已经为 Python 编程设置了环境。现在,您可以从 Python 的一个简单概念开始。Python 有两种基本模式——普通模式和交互模式。让我们详细看看这些模式。
对话方式
Python 的交互模式是一个命令行 shell,为每个执行的语句提供即时输出。它还将以前执行的语句的输出存储在活动内存中。当 Python 解释器执行新语句时,在评估当前输出时,会考虑之前执行的整个语句序列。你必须在 Linux/macOS 的命令提示符下输入python3,在 Windows 的命令提示符下输入python才能调用 Python 3 解释器进入交互模式,如下所示:
Python 3.4.2 (default, Oct 19 2014, 13:31:11)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
您可以在这种交互模式下直接执行 Python 语句,就像在操作系统外壳/控制台中运行命令一样,如下所示:
>>> print('Hello World!')
Hello World!
>>>
你不会在书中使用互动模式。然而,这是检查小代码片段(5 到 10 行)最快的方法。您可以使用exit()语句退出交互模式,如下所示:
>>> exit()
$
脚本模式
脚本模式是 Python 解释器执行 Python 脚本文件(.py)的模式。
创建一个名为test.py的文件,并将print ('Hello World!')语句添加到该文件中。保存文件并使用 Python 3 解释器运行它,如下所示。
$ python3 test.py
Hello World!
$
在本例中,python3是解释器,test.py是文件名。如果 Python test.py文件不在您调用python3解释器的同一个目录中,您必须提供 Python 文件的绝对路径。
Note
对于所有的 Linux 和 Mac 计算机,Python 3 解释器的命令是python3。对于 Windows,只是python,假设 Windows 电脑上只安装了 Python 3,并且在安装过程中或者安装后手动将其位置添加到PATH变量中。在本书中,我使用 Linux 命令提示符(在我的 Raspberry Pi 4 上)来运行代码示例。举几个例子,我会在使用 Windows 的地方明确提到它。
Python 的 ide
集成开发环境(IDE)是一个软件套件,拥有编写和测试程序的所有基本工具。典型的 IDE 有一个编译器、一个调试器、一个代码编辑器和一个构建自动化工具。大多数编程语言都有各种各样的 ide 让程序员过得更好。Python 也有许多 ide。让我们来看看 Python 的几个 ide。
闲置的
IDLE 代表集成开发环境。它与 Python 捆绑在一起。IDLE3 是针对 Python 3 的。很受 Python 初学者的欢迎。在安装了 Python 3 的 Linux 计算机上,只需在命令提示符下键入idle3。图 1-5 是 IDLE3 代码编辑器截图和交互提示。
图 1-5
IDLE3 在树莓 Pi 上运行
如果 IDLE 没有默认安装在您的 Linux 发行版上,那么您必须手动安装它。对于 Debian 及其衍生产品,安装命令如下:
sudo apt-get install idle
Eclipse 的 PyDev 插件
如果你是一个经验丰富的 Java 程序员,你可能已经在 Eclipse 上工作过。Eclipse 是一个非常流行的 Java IDE,它也可以用于其他编程语言。PyDev 是一个用于 Eclipse 的 Python IDE,它可以用于 Python、Jython 和 IronPython 开发(见图 1-6 )。您可以从位于 www.pydev.org 的 Eclipse marketplace 安装 PyDev。
图 1-6
PyDev 的 Eclipse
盖尼
Geany(见图 1-7 )是一个文本编辑器,它使用 GTK+工具包,具有集成开发环境的基本特性。它支持许多文件类型,并有一些不错的功能。查看 https://www.geany.org 了解更多详情。
图 1-7
盖尼
PyCharm
JetBrains 的 PyCharm 是 Python 的另一个 IDE,它包含了强大的功能,如调试器、代码检查工具、版本控制和集成的单元测试运行器。它是一个跨平台的 IDE,可用于 Windows、macOS 和 Linux 发行版。它的社区版是免费下载的。更多信息请访问其主页 https://www.jetbrains.com/pycharm/ 。
由于所涉及的库的性质,本书中的代码示例更适合在命令提示符下执行。我个人更喜欢把逻辑和代码写在纸上(没错!用一张纸!)然后使用带有语法突出显示的纯文本编辑器。比如我推荐 Windows 上的 Notepad++或者 Linux 上的 nano、Leafpad、gedit。您可以使用 IDLE3 或 Geany 来编写和编译代码。
然而,大多数代码示例都应该从命令行执行。
Exercise 1-1
-
访问并探索位于
www.python.org的 Python 主页。 -
在
https://www.python.org/about/success/阅读 Python 在哪里部署成功。 -
访问并浏览位于
https://docs.python.org/3/的 Python 文档页面。 -
在
https://docs.python.org/3/whatsnew/index.html查看 Python 最新版本的新特性。 -
为了练习,用 Python 编写简单的程序。例如,您可以使用迭代和递归技术为斐波那契数列和阶乘计算编写程序。
完成这个练习,更好地理解 Python 3 的背景。
结论
在本章中,你学习了 Python 的背景、历史和特性。您还学习了 Python 2 和 Python 3 之间的重要区别。您学习了如何在脚本和交互模式下安装和使用 Python 3。最后,您了解了一些流行的 Python ide。在下一章中,您将从测试自动化的概念开始,并查看一个简单的 Python 测试自动化库,名为doctest。你也简单看一下pydoc。
二、入门指南
在前一章中,您学习了如何在 Linux、macOS 和 Windows 计算机上设置 Python 3 环境。您还了解了一些流行的 Python ide。在这一章中,你将从测试自动化的概念开始。然后,您将探索一种学习 Python 3 中测试自动化框架的简单方法,称为doctest。
软件测试概念简介
教科书上对软件测试的定义是,它是执行一个程序或应用来发现任何错误的过程。通常,在软件测试的过程中有多个涉众。涉众包括测试人员、管理团队、顾问、业务、客户和最终用户。对于大中型项目,软件测试是为了确定软件在各种输入和条件下是否如预期的那样运行。
单元测试
单元测试是一种软件测试方法,在这种方法中,程序的单个组件,称为*单元,*使用所有需要的依赖项进行独立测试。单元测试大部分是由程序员完成的,他们为单元编写程序。在较小的项目中,这是非正式的。在大多数非常大规模的项目中,单元测试是正式开发过程的一部分,有适当的文档和适当的时间表/工作分配。
测试自动化
测试自动化是测试场景和案例结果的自动执行和报告。在大多数大型复杂的项目中,测试过程的许多阶段都是自动化的。有时候自动化测试的工作量是如此之大,以至于有一个独立的自动化项目,有一个独立的团队致力于此,包括一个独立的报告结构和独立的管理。有几个领域和阶段的测试可以自动化。像代码库和第三方 API 这样的各种工具被用于单元测试。有时候,单元测试的代码也是以自动化的方式生成的。单元测试是自动化的主要候选。
自动化单元测试的好处
自动化单元测试有很多原因。让我们逐一考虑。
- 时间和努力
随着代码库的增长,要进行单元测试的模块数量也在增长。手动测试非常耗时。为了减少手动测试的工作量,您可以自动化测试用例,然后可以简单快速地自动化测试用例。
- 准确
测试用例的执行是一个死记硬背和枯燥的活动。人类会犯错。然而,自动化测试套件每次都会运行并返回正确的结果。
- 早期错误报告
自动化单元测试用例给了你早期报告错误和错误的明显优势。当自动化测试套件由调度器运行时,一旦代码由于错误而冻结,代码中的所有逻辑错误都会被快速发现并报告,而不需要太多的人工干预。
- 对单元测试的内置支持
有许多编程语言通过专用于单元测试的库为编写单元测试提供内置支持。例子包括 Python、Java 和 PHP。
使用文档字符串
本章的重点是让你开始学习 Python 中的单元测试自动化。让我们从文档字符串的概念及其在 Python 中的实现开始。在学习doctest的时候,Docstrings 将会对你非常有用。
一个文档字符串是在模块的源代码中指定的字符串文字。它用于记录特定的代码段。代码注释也用于记录源代码。然而,文档字符串和注释之间有一个主要的区别。解析源代码时,注释不作为代码的一部分包含在解析树中,而文档字符串包含在解析的代码树中。
这样做的主要优点是文档字符串在运行时可用。使用特定于编程语言的功能,可以检索特定于模块的文档字符串。文档字符串总是在模块实例的整个运行时被保留。
Python 中的文档字符串示例
让我们看看如何在 Python 中实现 docstring 的概念。Python docstring 是作为模块、函数、类或方法定义中的第一条语句出现的字符串文字。docstring 成为该对象的 doc 特殊属性。
让我们看一个 Python docstring 的代码示例。从这一章开始,你将会做很多编程工作。我建议您在您的计算机上创建一个目录,并在其中创建特定章节的子目录。如前所述,我使用的是 Linux 操作系统。(我最喜欢的电脑,树莓 Pi 3 型号 b)我已经创建了一个名为book的目录和一个名为code的目录。code目录有包含每章代码的特定章节目录。图 2-1 以树形图的形式展示了目录结构的图形表示。
图 2-1
建议的图书目录结构
在code目录下创建章节子目录,如图 2-1 树形图所示。我们在这一章使用目录chapter02,在下一章使用chapter03,依此类推。导航到chapter02目录并将下面的代码(参见清单 2-1 )作为test_module01.py保存在该目录中。
"""
This is test_module01.
This is example of multiline docstring. """
class TestClass01:
"""This is TestClass01."""
def test_case01(self):
"""This is test_case01()."""
def test_function01():
"""This is test_function01()."""
Listing 2-1test_module01.py
在清单 2-1 中,有一个测试文件叫做test_module01.py,包含TestClass01和test_function01()。TestClass01有一种方法叫做test_ case01()。这里所有的代码单元都有一个 docstring。第一个 docstring 是多行 docstring。其余的都是单行 docstrings 的例子。
让我们使用清单 2-1 中的代码和一个交互式 Python 会话来看看 docstrings 是如何工作的。
导航到chapter02目录,键入python3以解释器模式调用 Python 3。
pi@raspberrypi:~/book/code/chapter02 $ pwd
/home/pi/book/code/chapter02
pi@raspberrypi:~/book/code/chapter02 $
python3 Python 3.4.2 (default, Oct 19 2014, 13:31:11)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
使用以下语句导入您刚刚创建的测试模块:
>>> import test_module01
您可以使用help()函数查看模块及其成员的文档字符串,如下所示。
>>> help(test_module01)
输出如下所示:
Help on module test_module01:
NAME
test_module01
DESCRIPTION
This is test_module01.
This is example of multiline docstring.
CLASSES
builtins.object
TestClass01
class TestClass01(builtins.object)
| This is TestClass01.
|
| Methods defined here:
|
| test_case01(self)
| This is test_case01().
|
|_________________________________________________
| Data descriptors defined here:
|
| __dict
| dictionary for instance variables (if defined)
|
| __weakref
| list of weak references to the object (if defined)
FUNCTIONS
test_function01()
This is test_function01().
FILE
/home/pi/book/code/chapter02/test_module01.py
您可以使用help()查看单个成员的 docstring。运行以下语句,亲自查看输出。
>>> help(test_module01.TestClass01)
>>> help(test_module01.TestClass01.test_case01)
>>> help(test_module01.test_function01)
如前所述,docstring 成为该对象的doc特殊属性。您还可以使用print()函数来查看模块及其成员的 docstring。以下交互式 Python 会话演示了这一点。
>>> import test_module01
>>> print(test_module01._doc_)
This is test_module01.
This is example of multiline docstring.
>>> print(test_module01.TestClass01._doc_)
This is TestClass01.
>>> print(test_module01.TestClass01.test_case01._doc_)
This is test_case01().
>>> print(test_module01.test_function01._doc_)
This is test_function01().
>>>
您可以在以下 PEP 页面上找到关于 Python docstring 的详细信息。
https://www.python.org/dev/peps/pep-0256
https://www.python.org/dev/peps/pep-0257
https://www.python.org/dev/peps/pep-0258
在下一节中,您将学习使用 docstrings 来编写简单的测试用例,并使用doctest来执行它们。
doctest 简介
doctest是 Python 中的轻量级单元测试框架,它使用文档字符串来测试自动化。doctest与 Python 解释器打包在一起,因此您不必单独安装任何东西来使用它。它是 Python 标准库的一部分,遵循 Python 的“包含电池”哲学。
Note
如果你感兴趣,你可以在 PEP 206 页面上阅读 Python 的电池内含哲学(参见 https://www.python.org/dev/peps/pep-0206 )。
清单 2-2 中的代码是一个测试模块的简单例子,它有两个函数,每个函数有两个测试。
"""
Sample doctest test module... test_module02
"""
def mul(a, b):
"""
>>> mul(2, 3)
6
>>> mul('a', 2)
'aa'
"""
return a * b
def add(a, b):
"""
>>> add(2, 3)
5
>>> add('a', 'b')
'ab'
"""
return a + b
Listing 2-2test_module02.py
在清单 2-2 中,测试用例被称为模块的文档字符串,代码本身并没有特别调用doctest。当程序作为 Python 3 程序使用python3 test命令执行时,_module02.py不会在命令行产生任何输出。为了查看doctest的运行情况,您必须在命令提示符下使用以下命令运行它:
python3 -m doctest -v test_module02.py
输出如下所示:
Trying:
add(2, 3)
Expecting:
5
ok
Trying:
add('a', 'b')
Expecting:
'ab'
ok
Trying:
mul(2, 3)
Expecting:
6
ok
Trying:
mul('a', 2)
Expecting:
'aa'
ok
1\. items had no tests:
test_module02
2\. items passed all tests:
2 tests in test_module02.add
2 tests in test_module02.mul
4 tests in 3 items.
4 passed and 0 failed.
Test passed.
我们来看看doctest是如何工作的。通过比较代码——特别是用于执行和输出的命令——您可以找出相当多的东西。doctest通过解析文档字符串来工作。每当doctest在一个模块的doctest文档中发现一个交互式 Python 提示,它就将其输出视为预期输出。然后,它通过引用文档字符串来运行模块及其成员。它将实际输出与文档字符串中指定的输出进行比较。然后,它标记测试通过或失败。你必须在执行模块时使用-m doctest来让解释器知道你需要使用doctest模块来执行代码。
命令行参数-v代表冗长模式。您必须使用它,因为没有它,测试将不会产生任何输出,除非它失败。不管测试是通过还是失败,使用 verbose 都会生成一个执行日志。
测试失败
在清单 2-2 中,所有的测试都顺利通过。现在,让我们看看测试是如何失败的。在清单 2-2 中,用*(星号)替换代码最后一行的+,并用相同的命令再次运行测试。您将获得以下输出:
Trying:
add(2, 3)
Expecting:
5
***************************************************************
File "/home/pi/book/code/chapter02/test_module02.py", line 19, in test_module02.add
Failed example:
add(2, 3)
Expected:
5
Got:
6
Trying:
add('a', 'b')
Expecting:
'ab'
***************************************************************
File "/home/pi/book/code/chapter02/test_module02.py", line 21, in test_module02.add
Failed example:
add('a', 'b')
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.4/doctest.py", line 1324, in_run
compileflags, 1), test.globs)
File "<doctest test_module02.add[1]>", line 1, in <module>
add('a', 'b')
File "/home/pi/book/code/chapter02/test_module02.py", line 24, in add
return a * b
TypeError: can't multiply sequence by non-int of type 'str'
Trying:
mul(2, 3)
Expecting:
6
ok
Trying:
mul('a', 2)
Expecting:
'aa'
ok
1 items had no tests:
test_module02
1 items passed all tests:
2 tests in test_module02.mul
***************************************************************
1 items had failures:
2 of 2 in test_module02.add
4 tests in 3 items.
2 passed and 2 failed.
***Test Failed*** 2 failures.
您可以在执行日志中清楚地看到两个失败。测试通常由于以下一个或多个原因而失败:
-
代码中的错误逻辑
-
错误的代码输入
-
错误的测试用例
在这种情况下,测试中有两个失败。第一个是由于错误的逻辑。第二个失败是由于代码中的错误逻辑和给被测试函数的错误输入类型。
通过将最后一行中的*替换为+来更正代码。然后将有'aa'的线路改为aa,再次运行测试。这将展示测试失败的第三个原因(一个错误的测试用例)。
单独的测试文件
您也可以在单独的测试文件中编写测试,并与要测试的代码分开运行。这有助于将测试模块/代码与开发代码分开维护。在同一个目录中创建一个名为test_module03.txt的文件,并将清单 2-3 中所示的代码添加到其中。
>>> from test_module02 import *
>>> mul(2, 3)
6
>>> mul('a', 2)
'aa'
>>> add(2, 3)
5
>>> add('a', 'b')
'ab'
Listing 2-3test_module03.txt
您可以通过在命令提示符下运行以下命令,以常规方式运行该测试:
python3 -m doctest -v test_module03.txt
输出如下所示:
Trying:
from test_module02 import *
Expecting nothing
ok
Trying:
mul(2, 3)
Expecting:
6
ok
Trying:
mul('a', 2)
Expecting:
'aa'
ok
Trying:
add(2, 3)
Expecting:
5
ok
Trying:
add('a', 'b')
Expecting:
'ab'
ok
1 items passed all tests:
5 tests in test_module03.txt
5 tests in 1 items.
5 passed and 0 failed.
Test passed.
doctest 的优点和缺点
正如您所了解的,doctest是一个非常简单直观的框架,适用于 Python 新手级别的测试。它不需要任何安装,你可以快速上手,不需要了解任何 API。它主要用于以下目的:
-
验证代码文档是否是最新的,以及在对代码进行更改后,文档字符串中的交互式示例是否仍然有效。
-
执行基于模块的基本回归测试。
-
编写说明性的教程和文档,作为包和模块的测试用例。
然而,doctest有它自己的一套限制。它没有真正用于测试的 API。
测试本质上也是静态的,不能参数化。
建议您访问 https://docs.python.org/3/library/doctest.html 的doctest文档页面,了解详细用法和更多示例。
Pydoc
就像doctest一样,还有另一个有用的工具来查看模块的文档。它是 Python 自带的。它被称为 Pydoc。在 Linux 上,运行以下命令:
pydoc unittest
它将显示unittest库的文档。如果您已经使用 docstrings 为自己的定制模块创建了文档,则可以使用以下命令查看它:
pydoc test_module01
该命令在终端上显示文档。您可以将所有这些信息保存在 HTML 文件中,如下所示:
pydoc -w unittest
pydoc -w test_module01
这些命令将在命令运行的目录中创建unittest.html和test_module01.html文档。然后,您可以用自己选择的 web 浏览器打开这些文件。
在 Windows 上,这些命令可以按如下方式运行:
python -m pydoc unittest
python -m pydoc -w unittest
结论
在这一章中,你学习了软件测试的基础知识。您探索了一个名为doctest的轻量级测试框架。对于 Python 新手来说,这是一个很好的简单项目模块。然而,由于缺乏 testrunner、test discovery 和 test fixtures 等高级特性,doctest并不用于大型项目。下一章讨论了 Python 内置的xUnit风格的测试自动化框架,称为unittest。
三、单元测试
最后一章讨论了测试自动化的概念。您了解了 docstring 和doctest以及它们在为 Python 3 程序编写简单、静态而优雅的测试用例中的用途。然而,由于缺乏 API、可配置测试和测试夹具等特性,doctest的受欢迎程度非常有限。你需要探索一个强大的 API 库来自动化复杂的现实项目,学习 Python 的内置unittest模块是你迈向它的第一步。
这是一个详细而漫长的章节。在这一章中,你会学到很多新概念,比如测试夹具、自动化测试发现、组织你的代码库等等。您将在整本书中使用这些概念,并在 Python 中更高级的测试自动化库中看到它们的实现。所以,我建议你仔细阅读本章的每一个主题。
unittest作为名为PyUnit的第三方模块诞生。PyUnit是JUnit的 Python 端口。JUnit是 Java 的xUnit风格的单元测试自动化框架。
从版本 2.5 开始,成为 Python 标准库的一部分。它被重新命名为unittest。unittest是 Python 的电池内置的测试自动化库,这意味着你不必安装额外的库或工具来开始使用它。任何熟悉其他编程语言中xUnit风格库的人(比如 Java 的JUnit、PHP 的PHPUnit、C++的CPPUnit等)。)会发现非常容易学习和使用unittest。
xUnit 简介
让我们简单地看一下xUnit哲学。xUnit是各种语言的几个单元测试框架的统称。所有xUnit风格的单元测试框架或多或少都从 Smalltalk 的单元测试框架(称为SUnit)中获得了它们的功能、结构和编码风格。肯特·贝克设计并创作了SUnit。它流行起来后,被 Kent Beck 和 Erich Gamma 移植到 Java 中作为JUnit。最终,它被移植到了几乎所有的编程语言中。现在大多数编程语言都预先打包了至少一个xUnit风格的测试自动化库。此外,许多编程语言,如 Python 和 Java,都有不止一个xUnit风格的框架。Java 除了JUnit还有TestNG。Python 除了unittest还有nose、pytest、Nose2。
所有xUnit风格的测试自动化库都遵循一个公共的架构。以下是该体系结构的主要组件:
-
测试用例类:这是测试模块中所有测试类的基类。所有的测试类都是从这里派生的。
-
测试夹具:这些是在测试代码块执行之前和之后运行的函数或方法。
-
断言:这些函数或方法用于检查被测试组件的行为。大多数
xUnit风格的框架都包含了强大的断言方法。 -
测试套件(Test suite):这是一组相关测试的集合,这些测试可以被执行或者被安排一起执行。
-
测试运行者:这是运行测试套件的程序或代码块。
-
测试结果格式化器(Test result formatter):它格式化测试结果,以各种人类可读的格式产生测试执行的输出,比如明文、HTML 和 XML。
xUnit的这些组件的实现细节在单元测试框架中略有不同。有趣的是,这使得程序员可以根据他们项目的需求和舒适度来选择框架。
如果您是一名经验丰富的程序员,并且有使用这些框架的经验,您将很快能够将您的知识转化为 Python 代码。如果你以前没有任何xUnit风格的框架的经验,那么在阅读了这本书,执行了书中所有的例子并解决了所有的练习之后,你就可以自己开始使用任何xUnit风格的框架,而不需要太多的动手操作。
使用单元测试
本节从unittest开始。它从测试类的最基本的概念开始。
对于本章,在code目录中创建一个名为chapter03的目录。在chapter03中,创建另一个名为test的目录(您将在本章后面了解为什么需要这个额外的目录)。将清单 3-1 中的代码保存为test_module01.py。
import unittest
class TestClass01(unittest.TestCase):
def test_case01(self):
my_str = "ASHWIN"
my_int = 999
self.assertTrue(isinstance(my_str, str))
self.assertTrue(isinstance(my_int, int))
def test_case02(self):
my_pi = 3.14
self.assertFalse(isinstance(my_pi, int))
if __name__ == '__main__':
unittest.main()
Listing 3-1test_module01.py
在清单 3-1 的代码中,import unittest语句导入了unittest模块。TestClass01是测试类。它是从unittest模块中的TestCase类派生出来的子类。test_case01()和test_case02()类方法是测试方法,因为它们的名字以test_开头(你将在本章的后面了解编写测试的指导方针和命名约定。)方法assertTrue()和assertFalse()是断言方法,分别检查传递给它们的参数是True还是False。如果自变量满足assert条件,测试用例通过;否则,它会失败。unittest.main()是试跑者。稍后您将详细探索更多的assert方法。
导航到测试目录,如下所示:
cd ~/book/code/chapter03/test
运行以下命令:
python3 test_module01.py
它产生以下输出:
---------------------------------------------------------
Ran 2 tests in 0.002s
OK
它显示OK,因为两个测试都通过了。这是编写和执行测试的方法之一。
测试执行没有显示太多信息。这是因为详细度在默认情况下是禁用的。您可以使用-v命令行选项在详细模式下运行测试。在命令提示符下运行以下命令:
python3 test_module01.py -v
详细输出如下:
test_case01 ( main .TestClass01) ... ok
test_case02 ( main .TestClass01) ... ok
---------------------------------------------------------
Ran 2 tests in 0.004s
OK
当然,详细执行模式提供了更多关于测试执行的信息。在整本书中,您将会非常频繁地使用这种模式来运行测试和收集测试执行的日志。
测试方法的执行顺序
现在,您将看到测试方法的执行顺序。查看清单 3-2 中的代码。
import unittest
import inspect
class TestClass02(unittest.TestCase):
def test_case02(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
def test_case01(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
if name == ' main ':
unittest.main(verbosity=2)
Listing 3-2test_module02.py
在清单 3-2 的代码中,inspect.stack() [0][3]方法打印当前测试方法的名称。当您想知道方法在测试类中的执行顺序时,这对于调试很有用。清单 3-2 中的代码输出如下:
test_case01 ( main .TestClass02) ...
Running Test Method : test_case01
ok
test_case02 ( main .TestClass02) ...
Running Test Method : test_case02
ok
---------------------------------------------------------
Ran 2 tests in 0.090s
OK
注意,测试方法是按字母顺序运行的,与代码中测试方法的顺序无关。
详细度控制
在前面的示例中,您在操作系统控制台中调用 Python 测试脚本时,通过命令控制测试执行的详细程度。现在,您将学习如何从代码本身控制详细模式。清单 3-3 中的代码展示了一个例子。
import unittest
import inspect
def add(x, y):
print("We're in custom made function : " + inspect.stack()[0][3])
return(x + y)
class TestClass03(unittest.TestCase):
def test_case01(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
self.assertEqual(add(2, 3), 5)
def test_case02(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
my_var = 3.14
self.assertTrue(isinstance(my_var, float))
def test_case03(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
self.assertEqual(add(2, 2), 5)
def test_case04(self):
print("\nRunning Test Method : " + inspect.stack()[0][3])
my_var = 3.14
self.assertTrue(isinstance(my_var, int))
if name == ' main ':
unittest.main(verbosity=2)
Listing 3-3test_module03.py
在清单 3-3 中,您正在用assertEqual()方法测试一个名为add()的定制函数。assertEqual()接受两个参数并判断两个参数是否相等。如果两个参数相等,测试用例通过;否则,它会失败。在同一个测试模块中还有一个名为add()的函数,它不是测试类的成员。用test_case01()和test_case03(),你在测试功能的正确性。
该代码还将 verbosity 设置为unittest.main()语句中的值2。
使用以下命令运行清单 3-3 中的代码:
python3 test_module03.py
输出如下所示:
test_case01 ( main .TestClass03) ...
Running Test Method : test_case01
We're in custom made function : add
ok
test_case02 ( main .TestClass03) ...
Running Test Method : test_case02
ok
test_case03 ( main .TestClass03) ...
Running Test Method : test_case03
We're in custom made function : add
FAIL
test_case04 ( main .TestClass03) ...
Running Test Method : test_case04
FAIL
===========================================================
FAIL: test_case03 ( main .TestClass03)
---------------------------------------------------------
Traceback (most recent call last):
File "test_module03.py", line 23, in test_case03
self.assertEqual(add(2, 2), 5)
AssertionError: 4 != 5
===========================================================
FAIL: test_case04 ( main .TestClass03)
---------------------------------------------------------
Traceback (most recent call last):
File "test_module03.py", line 28, in test_case04
self.assertTrue(isinstance(my_var, int))
AssertionError: False is not true
---------------------------------------------------------
Ran 4 tests in 0.112s
FAILED (failures=2)
因为assert条件失败,所以test_case03()和test_case04()测试用例失败。现在您有了更多关于测试用例失败的信息,因为代码中启用了详细性。
同一测试文件/模块中的多个测试类
到目前为止,这些示例在单个测试文件中包含了单个测试类。包含测试类的.py文件也被称为测试模块。清单 3-4 显示了一个拥有多个测试类的测试模块的例子。
import unittest
import inspect
class TestClass04(unittest.TestCase):
def test_case01(self):
print("\nClassname : " + self. class . name )
print("Running Test Method : " + inspect.stack()[0][3])
class TestClass05(unittest.TestCase):
def test_case01(self):
print("\nClassname : " + self. class . name )
print("Running Test Method : " + inspect.stack()[0][3])
if name == ' main ':
unittest.main(verbosity=2)
Listing 3-4test_module04.py
下面是运行清单 3-4 中的代码后的输出:
test_case01 ( main .TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
test_case01 ( main .TestClass05) ...
Classname : TestClass05
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 2 tests in 0.080s
OK
所有的测试类都是按照字母顺序逐一执行的。
测试夹具
简单来说,测试 夹具就是测试前后执行的一组步骤。
在unittest中,这些被实现为TestCase类的方法,并且可以根据您的需要被覆盖。清单 3-5 中显示了unittest的定制测试夹具示例。
import unittest
def setUpModule():
"""called once, before anything else in this module"""
print("In setUpModule()...")
def tearDownModule():
"""called once, after everything else in this module"""
print("In tearDownModule()...")
class TestClass06(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""called once, before any test"""
print("In setUpClass()...")
@classmethod
def tearDownClass(cls):
"""called once, after all tests, if setUpClass successful"""
print("In tearDownClass()...")
def setUp(self):
"""called multiple times, before every test method"""
print("\nIn setUp()...")
def tearDown(self):
"""called multiple times, after every test method"""
print("In tearDown()...")
def test_case01(self):
self.assertTrue("PYTHON".isupper())
print("In test_case01()")
def test_case02(self):
self.assertFalse("python".isupper())
print("In test_case02()")
if name == ' main ':
unittest.main()
Listing 3-5test_module05.py
在清单 3-5 的代码中,setUpModule()和tearDownModule()方法是模块级的 fixtures。setUpModule()是在测试模块中任何方法之前执行的。tearDownModule()是在测试模块中的所有方法之后执行。setUpClass()和tearDownClass()是级夹具。setUpClass()在测试类中的任何方法之前执行。tearDownClass()在测试类中的所有方法之后执行。
这些方法与@classmethod装饰器一起使用,如清单 3-5 中的代码所示。@classmethod装饰器必须引用一个类对象作为第一个参数。setUp()和tearDown()是方法级的夹具。setUp()和tearDown()方法在测试类中的每个测试方法之前和之后执行。运行清单 3-5 中的代码,如下所示:
python3 test_module05.py -v
以下是代码的输出:
In setUpModule()...
In setUpClass()...
test_case01 ( main .TestClass06) ...
In setUp()...
In test_case01()
In tearDown()...
ok
test_case02 ( main .TestClass06) ... In
setUp()...
In test_case02()
In tearDown()...
ok
In tearDownClass()...
In tearDownModule()...
---------------------------------------------------------
Ran 2 tests in 0.004s
OK
测试夹具及其实现是任何测试自动化库的关键特性。这是doctest提供的静态测试的主要优势。
不使用 unittest.main()运行
到目前为止,您已经使用unittest.main()运行了测试模块。现在您将看到如何在没有unittest.main()的情况下运行测试模块。例如,考虑清单 3-6 中的代码。
import unittest
class TestClass07(unittest.TestCase):
def test_case01(self):
self.assertTrue("PYTHON".isupper())
print("\nIn test_case01()")
Listing 3-6test_module06.py
如果你试图用常规方式运行它,用python3 test_module06.py,你不会在控制台得到输出,因为它没有if name ==' main '和unittest. main()语句。即使使用python3 test_module06.py -v在详细模式下运行也不会在控制台中产生任何输出。
运行该模块的唯一方法是使用带有-m unittest选项和模块名称的 Python 解释器,如下所示:
python -m unittest test_module06
输出如下所示:
In test_case01()
.
---------------------------------------------------------
Ran 1 test in 0.002s
OK
注意,您不需要像前面那样在模块名后面加上.py。您也可以使用-v选项启用详细度,如下所示:
python -m unittest test_module06 -v
详细输出如下:
test_case01 (test_module06.TestClass07) ...
In test_case01()
ok
---------------------------------------------------------
Ran 1 test in 0.002s
OK
您将在本章中使用相同的方法来运行测试模块。在本章后面的章节中,你将会学到更多关于这种方法的知识。现在,作为练习,使用这种执行方法运行前面的所有代码示例。
控制测试执行的粒度
您学习了如何使用-m unittest选项运行测试模块。您也可以使用这个选项运行单独的测试类和测试用例。
再次考虑前面的例子test_module04.py,如清单 3-7 所示。
import unittest
import inspect
class TestClass04(unittest.TestCase):
def test_case01(self):
print("\nClassname : " + self. class . name )
print("Running Test Method : " + inspect.stack()[0][3])
class TestClass05(unittest.TestCase):
def test_case01(self):
print("\nClassname : " + self. class . name )
print("Running Test Method : " + inspect.stack()[0][3])
if name == ' main ':
unittest.main(verbosity=2)
Listing 3-7test_module04.py
您可以使用以下命令运行整个测试模块:
python3 -m unittest -v test_module04
输出如下所示:
test_case01 (test_module04.TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
test_case01 (test_module04.TestClass05) ...
Classname : TestClass05
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 2 tests in 0.090s OK
您可以使用以下命令运行单个测试类:
python3 -m unittest -v test_module04.TestClass04
输出如下所示:
test_case01 (test_module04.TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 1 test in 0.077s
OK
您也可以使用以下命令运行单个测试用例:
python3 -m unittest -v test_module04.TestClass05.test_case01
输出如下所示:
test_case01 (test_module04.TestClass05) ...
Classname : TestClass05
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 1 test in 0.077s
OK
这样,您可以控制测试执行的粒度。
列出所有命令行选项和帮助
您可以使用-h命令行选项列出unittest的所有命令行选项。运行以下命令:
python3 -m unittest -h
以下是输出:
usage: python3 -m unittest [-h] [-v] [-q] [-f] [-c] [-b] [tests [tests ...]] positional arguments:
tests a list of any number of test modules, classes and test methods.
optional arguments:
-h, --help show this help message and exit
-v, --verbose Verbose output
-q, --quiet Quiet output
-f, --failfast Stop on first fail or error
-c, --catch Catch ctrl-C and display results so far
-b, --buffer Buffer stdout and stderr during tests
Examples:
python3 -m unittest test_module - run tests from test_module python3 -m unittest module.TestClass - run tests from module.
TestClass
python3 -m unittest module.Class.test_method - run specified test method
usage: python3 -m unittest discover [-h] [-v] [-q] [-f] [-c] [-b] [-s START] [-p PATTERN] [-t TOP]
optional arguments:
-h, --help show this help message and exit
-v, --verbose Verbose output
-q, --quiet Quiet output
-f, --failfast Stop on first fail or error
-c, --catch Catch ctrl-C and display results so far
-b, --buffer Buffer stdout and stderr during tests
-s START, --start-directory START
Directory to start discovery ('.' default)
-p PATTERN, --pattern PATTERN
Pattern to match tests ('test*.py' default)
-t TOP, --top-level-directory TOP
Top level directory of project (defaults to start directory)
For test discovery all test modules must be importable from the top level directory of the project.
通过这种方式,您可以获得unittest提供的各种命令行选项的详细摘要。
重要的命令行选项
让我们来看看unittest中重要的命令行选项。例如,看看清单 3-8 中的代码。
import unittest
class TestClass08(unittest.TestCase): def test_case01(self):
self.assertTrue("PYTHON".isupper())
print("\nIn test_case1()")
def test_case02(self):
self.assertTrue("Python".isupper()) print("\nIn test_case2()")
def test_case03(self):
self.assertTrue(True) print("\nIn test_case3()")
Listing 3-8test_module07.py
您已经知道-v代表详细模式。以下是详细模式下的输出:
test_case01 (test_module07.TestClass08) ...
In test_case1()
ok
test_case02 (test_module07.TestClass08) ... FAIL
test_case03 (test_module07.TestClass08) ...
In test_case3()
ok
=============================================
FAIL: test_case02 (test_module07.TestClass08)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module07.py", line 11, in test_case02
self.assertTrue("Python".isupper())
AssertionError: False is not true
---------------------------------------------------------
Ran 3 tests in 0.012s
FAILED (failures=1)
-q选项代表静音模式。运行以下命令演示安静模式:
python3 -m unittest -q test_module07
输出如下所示:
In test_case1()
In test_case3()
================================================
FAIL: test_case02 (test_module07.TestClass08)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module07.py", line 11, in test_case02
self.assertTrue("Python".isupper())
AssertionError: False is not true
---------------------------------------------------------
Ran 3 tests in 0.005s
FAILED (failures=1)
-f选项代表故障保护。一旦第一个测试用例失败,它就强制停止执行。运行以下命令启动故障保护模式:
python3 -m unittest -f test_module07
以下是故障保护模式下的输出:
In test_case1()
.F
=========================================================
FAIL: test_case02 (test_module07.TestClass08)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module07.py", line 11, in test_case02
self.assertTrue("Python".isupper())
AssertionError: False is not true
---------------------------------------------------------
Ran 2 tests in 0.004s
FAILED (failures=1)
您也可以使用多个选项。例如,您可以使用以下命令将 verbose 与 failsafe 结合使用:
python3 -m unittest -fv test_module07
输出如下所示:
test_case01 (test_module07.TestClass08) ...
In test_case1()
ok
test_case02 (test_module07.TestClass08) ... FAIL
==========================================================
FAIL: test_case02 (test_module07.TestClass08)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module07.py", line 11, in test_case02
self.assertTrue("Python".isupper())
AssertionError: False is not true
---------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (failures=1)
作为练习,尝试使用命令行选项的不同组合。
创建测试包
到目前为止,您已经单独创建并执行了测试模块。然而,您可以使用 Python 的内置打包特性来创建测试包。这是具有大型代码库的复杂项目中的标准做法。
图 3-1 显示了当前test目录的快照,在那里你保存你的测试模块。
图 3-1
第三章目录中测试子目录的快照
现在,让我们创建一个测试模块包。在test目录下创建一个init.py文件。将清单 3-9 中的代码添加到init.py文件中。
all = ["test_module01", "test_module02", "test_module03", "test_module04", "test_module05", "test_module06", "test_module07"]
Listing 3-9init.py
恭喜你!您刚刚创建了一个测试包。test是测试包的名称,init.py中提到的所有模块都属于这个包。如果您需要向test包中添加一个新的测试模块,您需要在测试目录中创建一个新的测试模块文件,然后将该模块的名称添加到init.py文件中。
现在,您可以通过下面的方式从test ( chapter03)的父目录中运行测试模块。使用以下命令移动到chapter03目录:
cd /home/pi/book/code/chapter03
注意,在您的情况下,路径可能会有所不同,这取决于您在哪里创建了book目录。
使用以下命令运行test模块:
python3 -m unittest -v test.test_module04
以下是输出:
test_case01 (test.test_module04.TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
test_case01 (test.test_module04.TestClass05) ...
Classname : TestClass05
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 2 tests in 0.090s
OK
使用以下命令在测试模块中运行测试类:
python3 -m unittest -v test.test_module04.TestClass04
输出如下所示:
test_case01 (test.test_module04.TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 1 test in 0.078s
OK
从测试模块运行测试用例,如下所示:
python3 -m unittest -v test.test_module04.TestClass04.test_case01
输出如下所示:
test_case01 (test.test_module04.TestClass04) ...
Classname : TestClass04
Running Test Method : test_case01
ok
---------------------------------------------------------
Ran 1 test in 0.079s
OK
组织代码
让我们来看看组织测试代码和开发代码的方法。您现在正转向使用unittest的真实项目场景。到目前为止,测试(测试代码)和要测试的代码(开发代码)在同一个模块中。通常在现实项目中,开发代码和测试代码保存在不同的文件中。
将开发和测试代码放在一个目录中
在这里,您将把开发和测试代码组织到一个目录中。在test目录中,创建一个名为test_me.py的模块,并将清单 3-10 中的代码添加到其中。
def add(x, y):
return(x + y)
def mul(x, y):
return(x * y)
def sub(x, y):
return(x - y)
def div(x, y):
return(x / y)
Listing 3-10test_me.py
现在,因为test_me.py在test目录中,所以可以使用import test_me语句将它直接导入到同一目录中的另一个模块中。清单 3-11 中的测试模块导入test_me.py来测试其功能。
import unittest
import test_me
class TestClass09(unittest.TestCase):
def test_case01(self):
self.assertEqual(test_me.add(2, 3), 5)
print("\nIn test_case01()")
def test_case02(self):
self.assertEqual(test_me.mul(2, 3), 6)
print("\nIn test_case02()")
Listing 3-11test_module08.py
使用以下命令运行测试模块:
python3 -m unittest -v test_module08
输出如下所示:
test_case01 (test_module08.TestClass09) ...
In test_case01()
ok
test_case02 (test_module08.TestClass09) ...
In test_case02()
ok
---------------------------------------------------------
Ran 2 tests in 0.004s OK
这样,您可以将开发代码和测试代码组织在同一目录的不同文件中。
将开发和测试代码放在不同的目录中
许多编码标准建议将开发代码和测试代码文件组织在不同的目录中。让我们现在做那件事。
导航到chapter03目录:
cd /home/pi/book/code/chapter03
在chapter03目录中创建一个名为mypackage的新目录:
mkdir mypackage
导航到mypackage目录:
cd mypackage
将清单 3-12 中的代码作为mymathlib.py保存在mypackage目录中。
class mymathlib:
def init (self):
"""Constructor for this class..."""
print("Creating object : " + self. class . name )
def add(self, x, y):
return(x + y)
def mul(self, x, y):
return(x * y)
def mul(self, x, y):
return(x - y)
def del (self):
"""Destructor for this class..."""
print("Destroying object : " + self. class . name )
Listing 3-12mymathlib.py
将清单 3-13 中的代码作为mymathsimple.py保存在mypackage目录中。
def add(x, y):
return(x + y)
def mul(x, y):
return(x * y)
def sub(x, y):
return(x - y)
def div(x, y):
return(x / y)
Listing 3-13mymathsimple.py
您刚刚创建的这些模块是开发模块。最后,为了创建一个开发模块包,用清单 3-14 中所示的代码创建init.py文件。
all = ["mymathlib", "mymathsimple"]
Listing 3-14init.py
这将为开发代码创建一个 Python 包。导航回chapter03目录。目录的结构现在应该如图 3-2 所示。
图 3-2
第三章目录的快照
mypackage是开发代码包,test是测试代码包。
您现在需要创建一个测试模块来测试mypackage中的开发代码。在test目录中创建一个名为test_module09.py的新测试模块,并添加清单 3-15 中所示的代码。
from mypackage.mymathlib import *
import unittest
math_obj = 0
def setUpModule():
"""called once, before anything else in the module"""
print("In setUpModule()...")
global math_obj
math_obj = mymathlib()
def tearDownModule():
"""called once, after everything else in the module"""
print("In tearDownModule()...")
global math_obj
del math_obj
class TestClass10(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""called only once, before any test in the class"""
print("In setUpClass()...")
def setUp(self):
"""called once before every test method"""
print("\nIn setUp()...")
def test_case01(self):
print("In test_case01()")
self.assertEqual(math_obj.add(2, 5), 7)
def test_case02(self):
print("In test_case02()")
def tearDown(self):
"""called once after every test method"""
print("In tearDown()...")
@classmethod
def tearDownClass(cls):
"""called once, after all the tests in the class"""
print("In tearDownClass()...")
Listing 3-15test_module09.py
将test_module09添加到test目录中的init.py中,使其成为test包的一部分。
使用以下命令运行test目录中的代码:
python3 -m unittest -v test_module09
它将抛出如下错误:
from mypackage.mymathlib import *
ImportError: No module named 'mypackage'
这是因为从test目录中看不到mypackage模块。它不在test目录中,而是在chapter03目录中。该模块不能从test目录执行。您必须将该模块作为test包的一部分来执行。您可以从chapter03目录中完成这项工作。mypackage模块在该目录中显示为mypackage,是chapter03的子目录。
导航到chapter03目录,按如下方式运行该模块:
python3 -m unittest -v test.test_module09
以下是执行的输出:
In setUpModule()...
Creating object : mymathlib
In setUpClass()...
test_case01 (test.test_module09.TestClass10) ...
In setUp()...
In test_case01()
In tearDown()...
ok
test_case02 (test.test_module09.TestClass10) ...
In setUp()...
In test_case02()
In tearDown()...
ok
In tearDownClass()...
In tearDownModule()...
Destroying object : mymathlib
---------------------------------------------------------
Ran 2 tests in 0.004s
OK
这就是如何在不同的目录中组织开发和测试代码文件。将这些代码文件分开是标准做法。
测试发现
测试发现是发现并执行项目目录及其所有子目录中所有测试的过程。测试发现过程在unittest中是自动化的,可以使用discover子命令调用。可以使用以下命令调用它:
python3 -m unittest discover
以下是该命令在chapter03目录中运行时的部分输出:
..
Running Test Method : test_case01
.
Running Test Method : test_case02
.
Running Test Method : test_case01
We're in custom made function : add
.
Running Test Method : test_case02
.
Running Test Method : test_case03
We're in custom made function : add
F
Running Test Method : test_case04
F
Classname : TestClass04
Running Test Method : test_case01
您也可以使用以下命令在详细模式下调用它:
python3 -m unittest discover -v
以下是该命令的部分输出:
test_case01 (test.test_module01.TestClass01) ... ok
test_case02 (test.test_module01.TestClass01) ... ok
test_case01 (test.test_module02.TestClass02) ...
Running Test Method : test_case01
ok
test_case02 (test.test_module02.TestClass02) ...
Running Test Method : test_case02
ok
test_case01 (test.test_module03.TestClass03) ...
Running Test Method : test_case01
We're in custom made function : add
ok
test_case02 (test.test_module03.TestClass03) ...
Running Test Method : test_case02
ok
test_case03 (test.test_module03.TestClass03) ...
Running Test Method : test_case03
We're in custom made function : add
测试发现有更多的命令行选项。您可以用-s或--start-directory指定起始目录。默认情况下,当前目录是起始目录。
您可以使用-p或--pattern作为文件名模式。test*.py是默认模式。
您可以使用-t或--top-level-directory来指定项目的顶层目录。默认值是起始目录。
正如您在详细输出中看到的,unittest自动找到并运行了位于chapter03目录及其子目录中的所有测试模块。这让您免去了单独运行每个测试模块并单独收集结果的痛苦。测试发现是任何自动化测试框架最重要的特性之一。
单元测试的编码约定
正如您所看到的,测试发现会自动发现并运行项目目录中的所有测试。为了达到这种效果,您需要为您的测试代码遵循一些编码和命名约定。你可能已经注意到,在本书的所有代码示例中,我一直遵循这些约定。
-
为了与测试发现兼容,所有测试文件必须是可从项目的顶级目录导入的模块或包。
-
默认情况下,测试发现总是从当前目录开始。
-
默认情况下,测试发现总是在文件名中搜索
test*.py模式。
单元测试中的断言
你已经学习了一些基本的断言,比如assertEqual()和assertTrue()。下表列出了最常用的断言及其用途。
Method
|
Checks That
|
| --- | --- |
| assertEqual(a, b) | a == b |
| assertNotEqual(a, b) | a != b |
| assertTrue(x) | bool(x) is True |
| assertFalse(x) | bool(x) is False |
| assertIs(a, b) | a is b |
| assertIsNot(a, b) | a is not b |
| assertIsNone(x) | x is None |
| assertIsNotNone(x) | x is not None |
| assertIn(a, b) | a in b |
| assertNotIn(a, b) | a not in b |
| assertIsInstance(a, b) | isinstance(a, b) |
| assertNotIsInstance(a, b) | not isinstance(a, b) |
| assertAlmostEqual(a, b) | round(a-b, 7) == 0 |
| assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 |
| assertGreater(a, b) | a > b |
| assertGreaterEqual(a, b) | a >= b |
| assertLess(a, b) | a < b |
| assertLessEqual(a, b) | a <= b |
| assertRegexpMatches(s, r) | r.search(s) |
| assertNotRegexpMatches(s, r) | not r.search(s) |
| assertItemsEqual(a, b) | sorted(a) == sorted(b) |
| assertDictContainsSubset(a, b) | all the key/value pairs in a exist in b |
| Method | Used to Compare |
| assertMultiLineEqual(a, b) | Strings |
| assertSequenceEqual(a, b) | Sequences |
| assertListEqual(a, b) | Lists |
| assertTupleEqual(a, b) | Tuples |
| assertSetEqual(a, b) | sets or frozensets |
| assertDictEqual(a, b) | Dicts |
在自动化测试时,上表中列出的所有assert方法对于大多数程序员和测试人员来说已经足够好了。
其他有用的方法
本节介绍一些有用的方法,它们将帮助您调试和理解执行流程。
id()和shortDescription()方法对于调试非常有用。id()返回方法的名称,shortDescription()返回方法的描述。清单 3-16 显示了一个例子。
import unittest
class TestClass11(unittest.TestCase):
def test_case01(self):
"""This is a test method..."""
print("\nIn test_case01()")
print(self.id())
print(self.shortDescription())
Listing 3-16test_module10.py
清单 3-16 的输出如下:
test_case01 (test_module10.TestClass11)
This is a test method... ...
In test_case01()
test_module10.TestClass11.test_case01
This is a test method...
ok
---------------------------------------------------------
Ran 1 test in 0.002s
OK
考试不及格
很多时候,您可能希望有一个方法在被调用时明确地使测试失败。在unittest中,fail()方法用于该目的。查看清单 3-17 中的代码作为示例。
import unittest
class TestClass12(unittest.TestCase):
def test_case01(self):
"""This is a test method..."""
print(self.id())
self.fail()
Listing 3-17test_module11.py
清单 3-17 的输出如下:
test_case01 (test_module11.TestClass12)
This is a test method... ...
test_module11.TestClass12.test_case01
FAIL
=========================================================
FAIL: test_case01 (test_module11.TestClass12)
This is a test method...
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module11.py", line 9, in test_case01
self.fail()
AssertionError: None
---------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)
Skipping tests
提供了一种有条件或无条件跳过测试的机制。
它使用以下装饰器来实现跳过机制:
-
unittest.skip(reason):无条件跳过修饰测试。reason应描述为何跳过测试。 -
unittest.skipIf(condition, reason):如果condition为真,跳过修饰测试。 -
unittest.skipUnless(condition, reason):跳过修饰测试,除非condition为真。 -
unittest.expectedFailure():将测试标记为预期失败。如果测试在运行时失败,则测试不算失败。
清单 3-18 中的代码演示了如何有条件地和无条件地跳过测试。
import sys
import unittest
class TestClass13(unittest.TestCase):
@unittest.skip("demonstrating unconditional skipping")
def test_case01(self):
self.fail("FATAL")
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_case02(self):
# Windows specific testing code
pass
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_case03(self):
# Linux specific testing code
pass
Listing 3-18test_module12.py
在 Linux 平台上运行清单 3-18 中的代码时,输出如下:
test_case01 (test_module12.TestClass13) ... skipped 'demonstrating unconditional skipping'
test_case02 (test_module12.TestClass13) ... skipped 'requires Windows'
test_case03 (test_module12.TestClass13) ... ok
---------------------------------------------------------
Ran 3 tests in 0.003s
OK (skipped=2)
当您在 Windows 平台上运行清单 3-18 中的代码时,输出如下:
test_case01 (test_module12.TestClass13) ... skipped 'demonstrating unconditional skipping'
test_case02 (test_module12.TestClass13) ... ok
test_case03 (test_module12.TestClass13) ... skipped 'requires Linux'
---------------------------------------------------------
Ran 3 tests in 0.003s
OK (skipped=2)
如您所见,代码根据运行的操作系统跳过了测试用例。这个技巧对于运行特定于平台的测试用例非常有用。
您也可以使用unittest. skip(reason)装饰器跳过测试模块中的整个测试类。
测试用例中的异常
当测试用例中出现异常时,测试用例失败。清单 3-19 中显示的代码将显式引发一个异常。
import unittest
class TestClass14(unittest.TestCase):
def test_case01(self):
raise Exception
Listing 3-19test_module13.py
清单 3-19 的输出如下:
test_case01 (test_module13.TestClass14) ... ERROR
=========================================================
ERROR: test_case01 (test_module13.TestClass14)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module13.py", line 6, in test_case01
raise Exception
Exception
---------------------------------------------------------
Ran 1 test in 0.004s
FAILED (errors=1)
测试因异常而失败时显示的失败消息与测试因断言而失败时不同。
assertRaises()
您了解到assert方法用于检查测试条件。assertRaises()方法用于检查代码块是否引发了assertRaises()中提到的异常。如果代码引发异常,则测试通过;否则,它会失败。清单 3-20 所示的代码详细演示了assertRaises()的用法。
import unittest
class Calculator:
def add1(self, x, y):
return x + y
def add2(self, x, y):
number_types = (int, float, complex)
if isinstance(x, number_types) and isinstance(y, number_ types):
return x + y
else:
raise ValueError
calc = 0
class TestClass16(unittest.TestCase):
@classmethod
def setUpClass(cls):
global calc
calc = Calculator()
def setUp(self):
print("\nIn setUp()...")
def test_case01(self):
self.assertEqual(calc.add1(2, 2), 4)
def test_case02(self):
self.assertEqual(calc.add2(2, 2), 4)
def test_case03(self):
self.assertRaises(ValueError, calc.add1, 2, 'two')
def test_case04(self):
self.assertRaises(ValueError, calc.add2, 2, 'two')
def tearDown(self):
print("\nIn tearDown()...")
@classmethod
def tearDownClass(cls):
global calc
del calc
Listing 3-20test_module14.py
清单 3-20 中的代码定义了一个名为Calculator的类,它有两种不同的加法运算方法。如果一个非数字的参数被传递给方法,add1()方法没有引发异常的规定。如果有任何参数是非数字的,add2()方法就会引发一个ValueError。下面是清单 3-20 中代码的输出:
test_case01 (test_module14.TestClass16) ...
In setUp()...
In tearDown()...
ok
test_case02 (test_module14.TestClass16) ...
In setUp()...
In tearDown()... ok
test_case03 (test_module14.TestClass16) ...
In setUp()...
In tearDown()...
ERROR
test_case04 (test_module14.TestClass16) ...
In setUp()...
In tearDown()...
ok
=============================================================
ERROR: test_case03 (test_module14.TestClass16)
---------------------------------------------------------
Traceback (most recent call last):
File "/home/pi/book/code/chapter03/test/test_module14.py", line 37, in test_case03
self.assertRaises(ValueError, calc.add1, 2, 'two')
File "/usr/lib/python3.4/unittest/case.py", line 704, in assertRaises
return context.handle('assertRaises', callableObj, args, kwargs)
File "/usr/lib/python3.4/unittest/case.py", line 162, in handle callable_obj(*args, **kwargs)
File "/home/pi/book/code/chapter03/test/test_module14.py", line 7, in add1
return x + y
TypeError: unsupported operand type(s) for +: 'int' and 'str'
---------------------------------------------------------
Ran 4 tests in 0.030s
FAILED (errors=1)
在输出中,test_Case03()失败是因为当您向它传递一个非数字参数(在本例中是一个字符串)时,add1()没有引发异常的规定。assertRaises()在编写负面测试用例时非常有用,比如当你需要针对无效参数检查 API 的行为时。
创建测试套件
您可以创建自己的定制测试套件和测试运行程序来运行这些测试套件。代码如清单 3-21 所示。
import unittest
def setUpModule():
"""called once, before anything else in this module"""
print("In setUpModule()...")
def tearDownModule():
"""called once, after everything else in this module"""
print("In tearDownModule()...")
class TestClass06(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""called once, before any test"""
print("In setUpClass()...")
@classmethod
def tearDownClass(cls):
"""called once, after all tests, if setUpClass successful"""
print("In tearDownClass()...")
def setUp(self):
"""called multiple times, before every test method"""
print("\nIn setUp()...")
def tearDown(self):
"""called multiple times, after every test method"""
print("In tearDown()...")
def test_case01(self):
self.assertTrue("PYTHON".isupper())
print("In test_case01()")
def test_case02(self):
self.assertFalse("python".isupper())
print("In test_case02()")
def suite():
test_suite = unittest.TestSuite()
test_suite.addTest(unittest.makeSuite(TestClass06))
return test_suite
if __name__ == '__main__':
mySuit=suite()
runner=unittest.TextTestRunner()
runner.run(mySuit)
Listing 3-21test_module16.py
该代码示例创建了一个套件,该套件为unittest.TestSuite()创建了一个对象。然后用addTest()方法将测试类添加到这个对象中。您可以向其中添加多个测试类。您也可以像那样创建多个测试套件。最后,这个例子在主体部分创建了这个测试套件类的一个对象。它还创建了一个testrunner对象,然后调用该对象来运行测试套件的对象。您可以创建多个测试套件,并在主要部分创建它们的对象。然后您可以使用testrunner对象来调用那些测试套件的对象。
创建测试套件
Exercise 3-1
像所有其他 Python 库一样,这是一个很大的主题,不可能在一本书中涵盖。因此,我建议您完成以下练习,以获得更多关于unittest的知识和经验。
-
请访问 Python 3 文档页面获取
unittest,网址为 -
实践本章中提到的所有断言方法,使用每一种方法编写测试。
-
练习使用
unittest.skipIf(condition, reason)和unittest.expectedFailure()装饰器。编写代码来演示它们的功能。 -
使用多个测试类编写一个测试模块,并使用
unittest.skip(reason)装饰器跳过整个测试类。 -
尝试在测试设备中引发异常。
提示通过启用每个注释掉的raise Exception行,一次一行,尝试运行清单 3-22 中的代码。这将帮助您理解当您在其中引发异常时,单个 fixture 的行为。
def setUpModule():
# raise Exception
pass
def tearDownModule():
# raise Exception
pass
class TestClass15(unittest.TestCase):
@classmethod
def setUpClass(cls):
# raise Exception
pass
def setUp(self):
# raise Exception
pass
def test_case01(self):
self.id()
def tearDown(self):
# raise Exception
pass
@classmethod
def tearDownClass(cls):
# raise Exception
Pass
Listing 3-22test_module15.py import unittest
结论
在这一章中,你学习了几个重要的概念,包括测试设备、测试类、测试方法、测试模块和测试套件。您还学习了如何用unittest实现所有这些概念。您还学习了断言和自动化测试发现。几乎你在本章中学到的所有概念都将在后面涉及其他 Python 测试框架的章节中重新讨论。下一章着眼于nose和nose2,这是另外两个流行的 Python 测试自动化和测试运行器框架。
我们在本章中学到的所有概念都是单元测试自动化领域的基础。我们将在整本书中使用它们,这些概念对于专业测试人员和开发人员非常有用。