Python-单元测试自动化教程-全-

Python 单元测试自动化教程(全)

原文:Python Unit Test Automation

协议:CC BY-NC-SA 4.0

一、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/ )中找到。在本书中,我们将探索一些用于测试自动化的库,如unittestnosenose2pytestselenium。我还参与了科学计算和计算机视觉库的工作并撰写了大量文章,比如numpyscipymatplotlibpillowscikit-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:

    1. 默认情况下,字符串是 Unicode 的

    2. 清除 Unicode/字节分隔

    3. 异常链接

    4. 函数注释

    5. 仅关键字参数的语法

    6. 扩展元组解包

    7. 非局部变量声明

从这个列表中,你会经常在本书的代码示例中使用到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 --Vpython3 --version来检查安装在您的 Linux 计算机上的 Python 3 解释器的版本。同样,你可以使用which python3命令来确定解释器在磁盘上的位置。

在 macOS X 上安装

在 macOS X 上,Python 2 解释器是默认安装的,可以使用python命令从终端调用。如果你想用 Python 3,你必须安装它。确保计算机连接到互联网,并在终端中运行brew install python3命令。这将安装 Python 3。它还将安装其他工具,如pipsetuptoolswheel

一旦安装完成,进入终端并键入python3 --Vpython3 --version来检查安装的 Python 3 解释器的版本。

在 Windows 上安装

在 Windows 操作系统中,Python 3 的安装需要更多的努力。Python 2 或 Python 3 没有预装在 Windows 计算机上。为了安装它,你必须在 https://www.python.org/downloads 访问 Python 网站的下载部分,如图 1-1 所示。

img/436414_2_En_1_Fig1_HTML.jpg

图 1-1

网站上的 Python 下载部分

选择 Python 3.5.2。(如果本书出版后有新的 Python 稳定版本,数字 5 和 2 可能会改变。)这将下载 Python 3 的安装文件。下载后打开安装文件。点击图 1-2 所示对话框中的运行按钮。

img/436414_2_En_1_Fig2_HTML.jpg

图 1-2

打开文件-安全警告对话框

Note

根据设置,您可能需要管理员权限才能在 Windows 计算机上安装 Python 3(或任何其他程序)。如果您处于组织环境中,请向您的系统管理团队咨询此信息。

如果您使用的是 Windows 7,根据您计算机的更新状态,您可能会遇到如图 1-3 所示的消息框。

img/436414_2_En_1_Fig3_HTML.jpg

图 1-3

Windows 7 的安装失败消息

通过安装任何 Windows 更新来更新操作系统,然后重新运行安装文件。成功后会出现图 1-4 中的窗口。

img/436414_2_En_1_Fig4_HTML.jpg

图 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 代码编辑器截图和交互提示。

img/436414_2_En_1_Fig5_HTML.jpg

图 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。

img/436414_2_En_1_Fig6_HTML.jpg

图 1-6

PyDev 的 Eclipse

盖尼

Geany(见图 1-7 )是一个文本编辑器,它使用 GTK+工具包,具有集成开发环境的基本特性。它支持许多文件类型,并有一些不错的功能。查看 https://www.geany.org 了解更多详情。

img/436414_2_En_1_Fig7_HTML.jpg

图 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

完成这个练习,更好地理解 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 以树形图的形式展示了目录结构的图形表示。

img/436414_2_En_2_Fig1_HTML.jpg

图 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,包含TestClass01test_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.htmldoctest文档页面,了解详细用法和更多示例。

Pydoc

就像doctest一样,还有另一个有用的工具来查看模块的文档。它是 Python 自带的。它被称为 Pydoc。在 Linux 上,运行以下命令:

pydoc unittest

它将显示unittest库的文档。如果您已经使用 docstrings 为自己的定制模块创建了文档,则可以使用以下命令查看它:

pydoc test_module01

该命令在终端上显示文档。您可以将所有这些信息保存在 HTML 文件中,如下所示:

pydoc -w unittest
pydoc -w test_module01

这些命令将在命令运行的目录中创建unittest.htmltest_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的第三方模块诞生。PyUnitJUnit的 Python 端口。JUnit是 Java 的xUnit风格的单元测试自动化框架。

从版本 2.5 开始,成为 Python 标准库的一部分。它被重新命名为unittestunittest是 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还有nosepytestNose2

所有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目录的快照,在那里你保存你的测试模块。

img/436414_2_En_3_Fig1_HTML.jpg

图 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.pytest目录中,所以可以使用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 所示。

img/436414_2_En_3_Fig2_HTML.jpg

图 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的知识和经验。

  1. 请访问 Python 3 文档页面获取unittest,网址为

    https://docs.python.org/3/library/unittest.html

  2. 实践本章中提到的所有断言方法,使用每一种方法编写测试。

  3. 练习使用unittest.skipIf(condition, reason)unittest.expectedFailure()装饰器。编写代码来演示它们的功能。

  4. 使用多个测试类编写一个测试模块,并使用unittest.skip(reason)装饰器跳过整个测试类。

  5. 尝试在测试设备中引发异常。

提示通过启用每个注释掉的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 测试框架的章节中重新讨论。下一章着眼于nosenose2,这是另外两个流行的 Python 测试自动化和测试运行器框架。

我们在本章中学到的所有概念都是单元测试自动化领域的基础。我们将在整本书中使用它们,这些概念对于专业测试人员和开发人员非常有用。

四、nosenose2

上一章介绍了xUnitunittest。在这一章中,你将探索 Python 的另一个单元测试 API,叫做nose。它的口号是 nose extends unittest 使测试更容易。

您可以使用nose的 API 来编写和运行自动化测试。你也可以使用nose来运行在其他框架中编写的测试,比如unittest。本章还探讨了下一个积极开发和维护的nosenose2的迭代。

nose入门

nose不是 Python 标准库的一部分。你必须安装它才能使用它。下一节将展示如何在 Python 3 上安装它。

在 Linux 发行版上安装 nose

在 Linux 计算机上安装nose最简单的方法是使用 Python 的包管理器pip来安装。Pip代表 pip 安装包。这是一个递归的缩写。如果您的 Linux 计算机上没有安装pip,您可以使用系统软件包管理器来安装它。在任何 Debian/Ubuntu 或衍生的计算机上,用下面的命令安装pip:

sudo apt-get install python3-pip

在 Fedora/CentOS 及其衍生产品上,运行以下命令(假设您在操作系统上安装了 Python 3.5)来安装pip:

sudo yum install python35-setuptools
sudo easy_install pip

一旦安装了pip,您可以使用以下命令安装nose:

sudo pip3 install nose

在 macOS 和 Windows 上安装 nose

pip在 macOS 和 Windows 上预装 Python 3。用以下命令安装nose:

pip3 install nose

验证安装

一旦安装了nose,运行以下命令来验证安装:

nosetests -V

它将显示如下输出:

nosetests version 1.3.7

在 Windows 上,此命令可能会返回错误,因此您也可以使用以下命令:

python -m nose -V

nose 入门

要从nose开始,请遵循与unittest相同的探索之路。在code目录下创建一个名为chapter04的目录,并将mypackage目录从chapter03目录复制到code。你以后会需要它的。创建一个名为test的目录。做完这些之后,chapter04目录结构应该如图 4-1 所示。

img/436414_2_En_4_Fig1_HTML.jpg

图 4-1

第四章目录结构

仅将所有代码示例保存到test目录。

一个简单的nose测试案例

清单 4-1 展示了一个非常简单的nose测试用例。

def test_case01():
    assert 'aaa'.upper() == 'AAA'

Listing 4-1test_module01.py

在清单 4-1 中,test_case01()是测试函数。assert是 Python 的内置关键字,它的工作方式类似于unittest中的assert方法。如果您将这段代码与unittest框架中最简单的测试用例进行比较,您会注意到您不需要从任何父类扩展测试。这使得测试代码更加整洁,不那么混乱。

如果您尝试使用以下命令运行它,它将不会产生任何输出:

python3 test_module01.py
python3 test_module01.py -v

这是因为您没有在代码中包含测试运行程序。

您可以使用 Python 的-m命令行选项来运行它,如下所示:

python3 -m nose test_module01.py

输出如下所示:

.
----------------------------------------------------------
Ran 1 test in 0.007s
OK

可以通过添加如下的-v命令行选项来调用详细模式:

python3 -m nose -v test_module01.py

输出如下所示:

test.test_module01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in 0.007s
OK

使用 nosetests 运行测试模块

您可以使用nosenosetests命令运行测试模块,如下所示:

nosetests test_module01.py

输出如下所示:

.
----------------------------------------------------------
Ran 1 test in 0.006s
OK

可以按如下方式调用详细模式:

nosetests test_module01.py -v

输出如下所示:

test.test_module01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in 0.007s
OK

使用nosetests命令是运行测试模块最简单的方法。由于编码和调用风格的简单和方便,我们将使用nosetests来运行测试,直到我们介绍和解释nose2。如果命令在 Windows 中返回一个错误,您可以用 Python 解释器调用nose模块。

获得帮助

使用以下命令获取关于nose的帮助和文档:

nosetests -h
python3 -m nose -h

组织测试代码

在前一章中,您学习了如何在不同的目录中组织项目的开发和测试代码。在这一章和下一章中,你也将遵循同样的标准。首先创建一个测试模块来测试mypackage中的开发代码。将清单 4-2 所示的代码保存在test目录中。

from mypackage.mymathlib import *

class TestClass01:
   def test_case01(self):
      print("In test_case01()")
      assert mymathlib().add(2, 5) == 7

Listing 4-2test_module02.py

清单 4-2 创建了一个名为TestClass01的测试类。如前所述,您不必从父类扩展它。包含assert的行检查语句mymathlib().add(2, 5) == 7是否为truefalse,以将测试方法标记为PASSFAIL

同样,创建一个init.py文件,将清单 4-3 中的代码放在test目录中。

all = ["test_module01", "test_module02"]

Listing 4-3init.py

在这之后,chapter04目录结构将类似于图 4-2 。

img/436414_2_En_4_Fig2_HTML.jpg

图 4-2

第四章目录结构

测试包现在准备好了。您可以从chapter04目录运行测试,如下所示:

nosetests test.test_module02 -v

输出如下所示:

test.test_module02.TestClass01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in 0.008s
OK

nose中,运行特定测试类的惯例有点不同。下面是一个例子:

nosetests test.test_module02:TestClass01 -v

您也可以按如下方式运行单独的测试方法:

nosetests test.test_module02:TestClass01.test_case01 -v

测试发现

您在前面的章节中学习了测试发现。nose还支持测试发现过程。事实上,nose中的测试发现甚至比unittest中的更简单。您不必使用discover子命令进行测试发现。您只需要导航到项目目录(本例中是chapter04)并运行nosetests命令,如下所示:

nosetests

您也可以在详细模式下调用此流程:

nosetests -v

输出如下所示:

test.test_module01.test_case01 ... ok test.test_module02.TestClass01.test_case01 ... ok
Ran 2 tests in 0.328s
OK

正如您在输出中看到的,nosetests自动发现测试包并运行它的所有测试模块。

类、模块和方法的夹具

nose提供了xUnit风格的夹具,其行为方式与unittest中的夹具相似。甚至灯具的名称也是一样的。考虑清单 4-4 中的代码。

from mypackage.mymathlib import *

math_obj = 0

def setUpModule():
    """called once, before anything else in this module"""
    print("In setUpModule()...")
    global math_obj
    math_obj = mymathlib()

def tearDownModule():
    """called once, after everything else in this module"""
    print("In tearDownModule()...")
    global math_obj del math_obj
class TestClass02:
    @classmethod
    def setUpClass(cls):
       """called once, before any test in the class"""
       print("In setUpClass()...")

    def setUp(self):
       """called before every test method"""
       print("\nIn setUp()...")

    def test_case01(self):
       print("In test_case01()")
       assert math_obj.add(2, 5) == 7

    def test_case02(self):
       print("In test_case02()")

    def tearDown(self):
      """called after every test method"""
      print("In tearDown()...")

    @classmethod
    def tearDownClass(cls):
       """called once, after all tests, if setUpClass() successful"""
       print ("\nIn tearDownClass()...")

Listing 4-4test_module03.py

如果用下面的命令运行清单 4-4 中的代码:

nosetests test_module03.py -v

输出如下所示:

test.test_module03.TestClass02.test_case01 ... ok test.test_module03.TestClass02.test_case02 ... ok
----------------------------------------------------------
Ran 2 tests in 0.010s
OK

为了获得关于测试执行的更多细节,您需要在命令行中添加-s选项,这允许任何stdout输出立即在命令行中打印出来。

运行以下命令:

nosetests test_module03.py -vs

输出如下所示:

In setUpModule()...
Creating object : mymathlib
In setUpClass()...
test.test_module03.TestClass02.test_case01 ...
In setUp()...
In test_case01()
In tearDown()...
ok
test.test_module03.TestClass02.test_case02 ...
In setUp()...
In test_case02()
In tearDown()...
ok

In tearDownClass()...
In tearDownModule()...
Destroying object : mymathlib
----------------------------------------------------------
Ran 2 tests in 0.011s
OK

从现在开始,在执行测试时,示例将把-s选项添加到nosetests命令中。

功能装置

在开始学习函数的 fixtures 之前,您必须理解 Python 中函数和方法之间的区别。一个函数是一段执行操作的命名代码,一个方法是一个带有额外参数的函数,该参数是它运行的对象。函数不与类相关联。一个方法总是与一个类相关联。

查看清单 4-5 中的代码作为例子。

from nose.tools import with_setup

def setUpModule():
    """called once, before anything else in this module"""
    print("\nIn setUpModule()...")

def tearDownModule():
    """called once, after everything else in this module"""
    print("\nIn tearDownModule()...")

def setup_function():
    """setup_function(): use it with @with_setup() decorator"""
    print("\nsetup_function()...")

def teardown_function():
    """teardown_function(): use it with @with_setup() decorator"""
    print("\nteardown_function()...")

def test_case01():
    print("In test_case01()...")

def test_case02():
    print("In test_case02()...")

@with_setup(setup_function, teardown_function)
def test_case03():
    print("In test_case03()...")

Listing 4-5test_module04.py

在清单 4-5 的代码中,test_case01()test_case02()test_case03()setup_ function()teardown_function()是函数。它们不与类相关联。你必须使用从nose.tools导入的@with_setup()装饰器,将setup_function()teardown_function()指定为test_case03()的夹具。nosetest_case01()test_case02()test_case03()识别为测试函数,因为由于@with_setup()装饰器,以test_. setup_function()teardown_function()开头的名称被识别为test_case03()的夹具。

test_case01()test_case02()功能没有分配任何夹具。

让我们用下面的命令运行这段代码:

nosetests test_module04.py -vs

输出如下所示:

In setUpModule()...
test.test_module04.test_case01 ... In test_case01()...
ok
test.test_module04.test_case02 ... In test_case02()...
ok
test.test_module04.test_case03 ... setup_function()...
In test_case03()...

teardown_function()...
ok

In tearDownModule()...
----------------------------------------------------------
Ran 3 tests in 0.011s
OK

正如您在输出中看到的,setup_function()teardown_function()分别在test_case03()之前和之后运行。unittest没有在测试功能级别提供夹具。实际上,unittest不支持独立测试函数的概念,因为所有的东西都必须从TestCase类扩展,而一个函数不能被扩展。

不一定要将函数级的 fixtures 命名为setup_function()teardown_function()。您可以随意命名它们(当然,除了 Python 3 的保留关键字)。只要你在@with_setup()装饰器中使用它们,它们就会在测试函数之前和之后被执行。

包装固定装置

unittest没有封装级夹具的规定。当测试包或测试包的一部分被调用时,包夹具被执行。将test目录中的init.py文件的内容更改为清单 4-6 中所示的代码。

all = ["test_module01", "test_module02", "test_module03", "test_module04"]

def setUpPackage():
    print("In setUpPackage()...")

def tearDownPackage():
    print("In tearDownPackage()...")

Listing 4-6init.py

如果您现在运行这个包中的一个模块,那么包级的 fixtures 将在开始任何测试之前以及包中的整个测试之后运行。运行以下命令:

nosetests test_module03.py -vs

以下是输出:

In setUpPackage()...
In setUpModule()...
Creating object : mymathlib
In setUpClass()...
test.test_module03.TestClass02.test_case01 ...
In setUp()...
In test_case01()
In tearDown()...
ok
test.test_module03.TestClass02.test_case02 ...
In setUp()...
In test_case02() In tearDown()...
ok

In tearDownClass()...
In tearDownModule()...
Destroying object : mymathlib
In tearDownPackage()...
----------------------------------------------------------
Ran 2 tests in 0.012s
OK

鼻固定装置的别名

该表列出了nose夹具的别名。

|

固定装置

|

替代名称

|
| --- | --- |
| setUpPackage | setup, setUp, or setup_package |
| tearDownPackage | teardown, tearDown, or teardown_package |
| setUpModule | setup, setUp, or setup_module |
| tearDownModule | teardown, tearDown, or teardown_module |
| setUpClass | setupClass, setup_class, setupAll, or setUpAll |
| tearDownClass | teardownClass, teardown_class, teardownAll, or tearDownAll |
| setUp (class method fixtures) | setup |
| tearDown (class method fixtures) | Teardown |

assert_equals()

到目前为止,您一直使用 Python 的内置关键字assert来对照预期值检查实际结果。nose对此自有assert_equals()的方法。清单 4-7 中的代码演示了assert_equals()assert的用法。

from nose.tools import assert_equals

def test_case01():
    print("In test_case01()...")
    assert 2+2 == 5

def test_case02():
    print("In test_case02()...")
    assert_equals(2+2, 5)

Listing 4-7test_module05.py

运行清单 4-7 中的代码。以下是输出:

In setUpPackage()...
test.test_module05.test_case01 ... In test_case01()...
FAIL
test.test_module05.test_case02 ... In test_case02()...
FAIL
In tearDownPackage()...

============================================================
FAIL: test.test_module05.test_case01
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/home/pi/book/code/chapter04/test/test_module05.py", line 6, in test_case01
   assert 2+2 == 5
AssertionError
===========================================================
FAIL: test.test_module05.test_case02
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/home/pi/book/code/chapter04/test/test_module05.py", line 11, in test_case02
   assert_equals(2+2, 5)
AssertionError: 4 != 5
----------------------------------------------------------
Ran 2 tests in 0.013s
FAILED (failures=2)

由于不正确的测试输入,两个测试案例都失败了。请注意这些测试方法打印的日志之间的差异。在test_case02()中,你会得到更多关于失败原因的信息,因为你使用的是noseassert_equals()方法。

测试工具

有一些方法和装饰器在你自动化测试时会非常方便。这一节将介绍其中的一些测试工具。

ok_ 和 eq_

ok_eq_分别是assertassert_equals()的简称。当测试用例失败时,它们还带有一个错误消息的参数。清单 4-8 中的代码演示了这一点。

from nose.tools import ok_, eq_

def test_case01():
    ok_(2+2 == 4, msg="Test Case Failure...")

def test_case02():
    eq_(2+2, 4, msg="Test Case Failure...")

def test_case03():
    ok_(2+2 == 5, msg="Test Case Failure...")

def test_case04():
    eq_(2+2, 5, msg="Test Case Failure...")

Listing 4-8test_module06.py

下面显示了清单 4-8 中代码的输出。

In setUpPackage()... test.test_module06.test_case01 ... ok test.test_module06.test_case02 ... ok test.test_module06.test_case03 ... FAIL test.test_module06.test_case04 ... FAIL
In tearDownPackage()...

===========================================================
FAIL: test.test_module06.test_case03
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/home/pi/book/code/chapter04/test/test_module06.py", line 13, in test_case03
   ok_(2+2 == 5, msg="Test Case Failure...")
AssertionError: Test Case Failure...

============================================================
FAIL: test.test_module06.test_case04
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/home/pi/book/code/chapter04/test/test_module06.py", line 17, in test_case04
   eq_(2+2, 5, msg="Test Case Failure...")
AssertionError: Test Case Failure...
----------------------------------------------------------
Ran 4 tests in 0.015s
FAILED (failures=2)

@raises()装饰器

当您在测试之前使用raises装饰器时,它必须引发与@raises()装饰器相关的异常列表中提到的一个异常。清单 4-9 展示了这个想法。

from nose.tools import raises

@raises(TypeError, ValueError)
def test_case01():
    raise TypeError("This test passes")

@raises(Exception)
def test_case02():
    pass

Listing 4-9test_module07.py

输出如下所示:

In setUpPackage()...
test.test_module07.test_case01 ... ok test.test_module07.test_case02 ... FAIL
In tearDownPackage()...

===========================================================
FAIL: test.test_module07.test_case02
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/usr/local/lib/python3.4/dist-packages/nose/tools/nontrivial.py", line 67, in newfunc
   raise AssertionError(message)
AssertionError: test_case02() did not raise Exception
----------------------------------------------------------
Ran 2 tests in 0.012s
FAILED (failures=1)

如您所见,test_case02()失败了,因为它没有在应该引发异常时引发异常。你可以巧妙地利用这一点来编写负面的测试用例。

@timed()装饰器

如果您在测试中使用一个定时装饰器,测试必须在@timed()装饰器中提到的时间内完成才能通过。清单 4-10 中的代码演示了这个想法。

from nose.tools import timed
import time

@timed(.1)
def test_case01():
    time.sleep(.2)

Listing 4-10test_module10.py

这个测试失败了,因为它花费了比@timed()装饰器中分配的更多的时间来完成测试。执行的输出如下:

In setUpPackage()...
test.test_module08.test_case01 ... FAIL
In tearDownPackage()...

=========================================================
FAIL: test.test_module08.test_case01
----------------------------------------------------------
Traceback (most recent call last):
   File "/usr/local/lib/python3.4/dist-packages/nose/case.py", line 198, in runTest
   self.test(*self.arg)
   File "/usr/local/lib/python3.4/dist-packages/nose/tools/nontrivial.py", line 100, in newfunc
   raise TimeExpired("Time limit (%s) exceeded" % limit) nose.tools.nontrivial.TimeExpired: Time limit (0.1) exceeded
----------------------------------------------------------
Ran 1 test in 0.211s
FAILED (failures=1)

它是可以一起执行或计划一起执行的相关测试的集合或组。

报表生成

让我们看看使用nose生成可理解的报告的各种方法。

创建 XML 报告

nose有一个生成 XML 报告的内置特性。这些是xUnit风格的格式化报告。你必须使用--with-xunit来生成报告。报告在当前工作目录中生成。

test目录中运行以下命令:

nosetests test_module01.py -vs --with-xunit

输出如下所示:

In setUpPackage()...
test.test_module01.test_case01 ... ok
In tearDownPackage()...
----------------------------------------------------------
XML: /home/pi/book/code/chapter04/test/nosetests.xml
----------------------------------------------------------
Ran 1 test in 0.009s
OK

生成的 XML 文件如清单 4-11 所示。

<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="nosetests" tests="1" errors="0" failures="0" skip="0">
<testcase classname="test.test_module01" name="test_case01" time="0.002">
</testcase>
</testsuite>

Listing 4-11nosetests.xml

创建 HTML 报告

nose没有内置的 HTML 报告功能。你必须为此安装一个插件。运行以下命令安装 HTML 输出插件:

sudo pip3 install nose-htmloutput

安装插件后,您可以运行以下命令来执行测试:

nosetests test_module01.py -vs --with-html

以下是输出:

In setUpPackage()...
test.test_module01.test_case01 ... ok
In tearDownPackage()...
----------------------------------------------------------
HTML: nosetests.html
----------------------------------------------------------
Ran 1 test in 0.009s
OK

该插件将输出保存在名为nosetests.html的文件中的当前位置。

图 4-3 显示了在网络浏览器中打开的nosetests.html文件的快照。

img/436414_2_En_4_Fig3_HTML.jpg

图 4-3

nosetests.html 档案

在控制台中创建彩色输出

到目前为止,您已经看到了生成格式化输出文件的方法。运行nosetest时,您一定已经观察到控制台输出是单色的(黑色背景上的白色文本,反之亦然)。名为rednose的插件用于创建彩色的控制台输出。您可以使用以下命令安装该插件:

sudo pip3 install rednose

安装插件后,运行以下命令:

nosetests test_module08.py -vs --rednose

图 4-4 显示了输出的屏幕截图,尽管由于已出版书籍的灰度特性,您在这里看不到彩色的。

img/436414_2_En_4_Fig4_HTML.jpg

图 4-4

nose示范

从 nose 运行 unittest 测试

在本章的开始,你读到了你可以用nose运行unittest测试。让我们现在试试。导航到chapter03目录。运行以下命令,自动发现并执行所有的unittest测试:

nosetests -v

这是输出:

test_case01 (test.test_module01.TestClass01) ... ok
test_case02 (test.test_module01.TestClass01) ... ok
test_case01 (test.test_module02.TestClass02) ... ok
test_case02 (test.test_module02.TestClass02) ... ok
test_case01 (test.test_module03.TestClass03) ... ok
test_case02 (test.test_module03.TestClass03) ... ok
test_case03 (test.test_module03.TestClass03) ... FAIL test_case04 (test.test_module03.TestClass03) ... FAIL test_case01 (test.test_module04.TestClass04) ... ok

我截断了输出,否则它会填满许多页面。自己运行命令来查看整个输出。

从 nose 运行 doctest 测试

您可以从nose运行doctest测试,如下所示。首先导航到保存doctest测试的目录:

cd ~/book/code/chapter02

然后按如下方式运行测试:

nosetests -v

输出如下所示:

This is test_case01(). ... ok
This is test_function01(). ... ok

----------------------------------------------------------
Ran 2 tests in 0.007s

OK

nose 优于 unittest 的优势

下面总结一下nose相对于unittest的优势:

  • unittest不同,nose不需要你从父类中扩展测试用例。这导致更少的代码。

  • 使用nose,可以编写测试函数。这在unittest中是不可能的。

  • noseunittest拥有更多的夹具。除了常规的unittest夹具,nose还有包级和功能级夹具。

  • nose有夹具的替代名称。

  • 为自动化测试用例提供了许多特性。

  • 测试发现在nose中比在unittest中更简单,因为nose不需要带有discover子命令的 Python 解释器。

  • nose可以轻松识别和运行unittest测试。

nose的缺点

nose唯一也是最大的缺点是,它没有处于积极的开发中,过去几年一直处于维护模式。如果没有新的人或团队来接管维护工作,它很可能会停止。如果你计划开始一个项目,并且正在为 Python 3 寻找一个合适的自动化框架,你应该使用pytestnose2或者普通的unittest

你可能会奇怪,如果它没有被积极地开发,我为什么还要花时间去讨论nose。原因是学习像nose这样更高级的框架有助于你理解unittest的局限性。此外,如果您正在使用一个使用nose作为测试自动化和/或单元测试框架的老项目,它将帮助您理解您的测试。

使用 nose2

nose2是 Python 的下一代测试。它基于unittest2的插件分支。

nose2旨在从以下方面对nose进行改进:

  • 它提供了一个更好的插件 API。

  • 用户更容易配置。

  • 它简化了内部接口和流程。

  • 它支持来自相同代码库的 Python 2 和 3。

  • 它鼓励社区更多地参与其发展。

  • nose不同,它正在积极开发中。

nose2可以使用以下命令方便地安装:

sudo pip3 install nose2

安装后,可以通过在命令提示符下运行nose2来调用nose2

它可用于自动发现和运行unittestnose测试模块。在命令提示符下运行nose2 -h命令,获得各种nose2命令行选项的帮助。

以下是nosenose2的重要区别:

  • Python 版本

nose支持 Python 及以上版本。nose2支持 pypy,2.6,2.7,3.2,3.3,3.4,3.5。nose2不支持所有版本,因为不可能在一个代码库中支持所有 Python 版本。

  • 测试负载

nose逐个加载并执行测试模块,称为懒加载。相反,nose2首先加载所有模块,然后一次执行所有模块。

  • 测试发现

由于测试加载技术的不同,nose2并不支持所有的项目布局。图 4-5 所示的布局由nose支撑。但是,nose2不会正确加载。nose可以区分./dir1/test.py./dir1/dir2/test.py

img/436414_2_En_4_Fig5_HTML.jpg

图 4-5

nose2 不支持的测试布局

您可以使用nose2运行测试,如下所示:

nose2 -v

您还可以参数化测试,如清单 4-12 所示。

from nose2.tools import params

@params("Test1234", "1234Test", "Dino Candy")
def test_starts_with(value):
    assert value.startswith('Test')

Listing 4-12test_module09.py

您可以按如下方式运行测试:

nose2 -v

或者

python -m nose2 test_module09

输出如下所示:

.FF
=============================================================
FAIL: test_module09.test_starts_with:2
'1234Test'
-------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Ashwin\Google Drive\Python Unit Test Automation - Second Edition\Code\chapter04\test\test_module09.py", line 5, in test_starts_with
    assert value.startswith('Test')
AssertionError

==============================================================
FAIL: test_module09.test_starts_with:3
'Dino Candy'
--------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Ashwin\Google Drive\Python Unit Test Automation - Second Edition\Code\chapter04\test\test_module09.py", line 5, in test_starts_with
    assert value.startswith('Test')
AssertionError

----------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (failures=2)

您可以通过修改代码直接从任何 IDE 启动测试脚本,而无需指定nose2模块,如清单 4-13 所示。

from nose2.tools import params

@params("Test1234", "1234Test", "Dino Candy")
def test_starts_with(value):
    assert value.startswith('Test')

if __name__ == '__main__':
    import nose2
    nose2.main()

Listing 4-13test_module20.py

您可以直接从任何 IDE(如 IDLE)启动它,它会产生相同的结果。

Exercise 4-1

检查您组织中的代码库是否在使用unittestnosenose2。咨询代码库的所有者,计划从这些框架到更好、更灵活的单元测试框架的迁移。

结论

在本章中,你学习了高级单元测试框架nose。不幸的是,它没有被积极开发,所以你需要使用nose2作为nose测试的测试员。在下一章中,您将了解并探索一个叫做py.test的高级测试自动化框架。

五、pytest

在第四章中,您探索了nose,这是一个用于 Python 测试的高级且更好的框架。不幸的是,nose在过去的几年里没有得到积极的开发。当你想为一个长期项目选择一些东西时,这使得它不适合作为测试框架的候选。此外,有许多项目使用unittestnose或两者的组合。你肯定需要一个比unittest有更多功能的框架,而且不像nose,它应该在积极开发中。nose2更像是unittest的测试版,几乎是一个废弃的工具。你需要一个单元测试框架,能够发现和运行用unittestnose编写的测试。它应该是先进的,并且必须得到积极的开发、维护和支持。答案是pytest

本章广泛地探索了一个现代的、先进的、更好的测试自动化框架,称为pytest。首先,你将了解pytest如何提供传统的xUnit风格的夹具,然后你将探索pytest提供的先进夹具。

pytest 简介

pytest不是 Python 标准库的一部分。你必须安装它才能使用它,就像你安装了nosenose2一样。让我们看看如何为 Python 3 安装它。pytest可以通过在 Windows 中运行以下命令方便地安装:

pip install pytest

对于 Linux 和 macOS,使用pip3安装它,如下所示:

sudo pip3 install pytest

这将为 Python 3 安装pytest。它可能会显示一个警告。警告消息中会有一个目录名。我用的是一个树莓 Pi,用的是树莓 Pi OS 作为 Linux 系统。它使用 bash 作为默认 shell。将下面一行添加到。bashrc和。bash_profile目录下的文件。

PATH=$PATH:/home/pi/.local/bin

将这一行添加到文件后,重新启动 shell。现在,您可以通过运行以下命令来检查安装的版本:

py.test --version

输出如下所示:

pytest 6.2.5

简单测试

在开始之前,在code目录中创建一个名为chapter05的目录。从chapter04目录复制mypackage目录。在chapter05中创建一个名为test的目录。将本章的所有代码文件保存在test目录中。

就像使用nose的时候,写一个简单的测试非常容易。参见清单 5-1 中的代码作为例子。

def test_case01():
    assert 'python'.upper() == 'PYTHON'

Listing 5-1test_module01.py

清单 5-1 进口pytest在第一行。test_case01()是测试函数。回想一下assert是 Python 内置的关键字。同样,就像使用nose一样,您不需要从任何类中扩展这些测试。这有助于保持代码整洁。

使用以下命令运行测试模块:

python3 -m pytest test_module01.py

输出如下所示:

===================== test session starts ====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 rootdir: /home/pi/book/code/chapter05/test, inifile:
collected 1 items

test_module01.py .
================== 1 passed in 0.05 seconds =================

您也可以使用详细模式:

python3 -m pytest -v test_module01.py

输出如下所示:

=============== test session starts ===========================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile: collected 1 items

test_module01.py::test_case01 PASSED

================ 1 passed in 0.04 seconds ====================

使用 py.test 命令运行测试

您也可以使用pytest's自己的命令运行这些测试,称为py.test:

py.test test_module01.py

输出如下所示:

================= test session starts =======================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 rootdir: /home/pi/book/code/chapter05/test, inifile:
collected 1 items

test_module01.py .
=============== 1 passed in 0.04 seconds ===================

您也可以使用详细模式,如下所示:

py.test test_module01.py -v

详细模式下的输出如下:

=================== test session starts =======================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile:
collected 1 items

test_module01.py::test_case01 PASSED
==================== 1 passed in 0.04 seconds =================

为了简单和方便起见,从现在开始,在本章和本书的剩余部分,您将使用相同的方法来运行这些测试。你将在最后一章中使用pytest来实现一个具有测试驱动开发方法的项目。此外,当您运行您自己的测试时,请注意测试执行的输出默认是彩色的,尽管这本书显示的结果是黑白的。你不必使用任何外部或第三方插件来实现这一效果。图 5-1 显示了一个执行样本的截图。

img/436414_2_En_5_Fig1_HTML.jpg

图 5-1

pytest 执行示例

pytest 中的测试类和测试包

像所有以前的测试自动化框架一样,在pytest中,您可以创建测试类和测试包。以清单 5-2 中的代码为例。

class TestClass01:

    def test_case01(self):
        assert 'python'.upper() == 'PYTHON'

    def test_case02(self):
        assert 'PYTHON'.lower() == 'python'

Listing 5-2test_module02.py

还要创建一个init.py文件,如清单 5-3 所示。

all = ["test_module01", "test_module02"]

Listing 5-3_init.py

现在导航到chapter05目录:

cd /home/pi/book/code/chapter05

并运行测试包,如下所示:

py.test test

您可以通过运行前面的命令来查看输出。您还可以使用以下命令在详细模式下运行测试包。

py.test -v test

您可以使用以下命令运行包中的单个测试模块:

py.test -v test/test_module01.py

您还可以运行特定的测试类,如下所示:

py.test -v test/test_module02.py::TestClass01

您可以运行特定的测试方法,如下所示:

py.test -v test/test_module02.py::TestClass01::test_case01

您可以运行特定的测试功能,如下所示:

py.test -v test/test_module01.py::test_case01

pytest 中的测试发现

pytest可以发现并自动运行测试,就像unittestnosenose2一样。在project目录中运行以下命令来启动自动化测试发现:

py.test

对于详细模式,运行以下命令:

py.test -v

xUnit 风格的灯具

pytestxUnit样式的夹具。请参见清单 5-4 中的代码作为示例。

def setup_module(module):
    print("\nIn setup_module()...")

def teardown_module(module):
    print("\nIn teardown_module()...")

def setup_function(function):
    print("\nIn setup_function()...")

def teardown_function(function):
    print("\nIn teardown_function()...")

def test_case01():
   print("\nIn test_case01()...")

 def test_case02():
    print("\nIn test_case02()...")

class TestClass02:

   @classmethod
   def setup_class(cls):
      print ("\nIn setup_class()...")

   @classmethod
   def teardown_class(cls):
      print ("\nIn teardown_class()...")

   def setup_method(self, method):
      print ("\nIn setup_method()...")

   def teardown_method(self, method):
      print ("\nIn teardown_method()...")

   def test_case03(self):
      print("\nIn test_case03()...")

   def test_case04(self):
      print("\nIn test_case04()...")

Listing 5-4test_module03.py

在这段代码中,setup_module()teardown_module()是模块级的 fixtures,它们在模块中的任何东西之前和之后被调用。setup_class()teardown_class()是类级别的固定装置,它们在类中的任何东西之前和之后运行。你必须使用@classmethod()装饰器。setup_method()teardown_method()是在每个测试方法之前和之后运行的方法级夹具。setup_function()teardown_function()是在模块中每个测试函数之前和之后运行的函数级 fixtures。在nose中,您需要带有测试函数的@with_setup()装饰器来将这些函数分配给函数级 fixtures。在pytest中,功能级夹具默认分配给所有测试功能。

同样,就像使用nose一样,您需要使用-s命令行选项来查看命令行上的详细日志。

现在运行带有额外的-s选项的代码,如下所示:

py.test -vs test_module03.py

接下来,使用以下命令再次运行测试:

py.test -v test_module03.py

比较这些执行模式的输出,以便更好地理解。

对 unittest 和 nose 的 pytest 支持

pytest支持unittestnose中编写的所有测试。pytest可以自动发现并运行unittestnose中编写的测试。它支持所有用于unittest测试类的xUnit风格的夹具。它还支持nose中的大部分夹具。尝试运行chapter03chapter04目录中的py.test -v

pytest 夹具介绍

除了支持xUnit风格的夹具和unittest夹具,pytest有自己的一套灵活、可扩展和模块化的夹具。这是pytest的核心优势之一,也是为什么它是自动化测试人员的热门选择。

pytest中,您可以创建一个夹具,并在需要的地方将其作为资源使用。

以清单 5-5 中的代码为例。

import pytest

@pytest.fixture()
def fixture01():
    print("\nIn fixture01()...")

def test_case01(fixture01):
    print("\nIn test_case01()...")

Listing 5-5test_module04.py

在清单 5-5 ,fixture01()是 fixture 函数。这是因为你使用了@pytest.fixture()装饰器。test_case01()是一个使用fixture01()的测试功能。为此,您将把fixture01作为参数传递给test_case01()

以下是输出:

=================== test session starts ======================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile: collected 1 items

test_module04.py::test_case01
In fixture01()...

In test_case01()...
PASSED

================= 1 passed in 0.04 seconds ====================

如您所见,fixture01()在测试函数test_case01()之前被调用。你也可以使用@pytest.mark.usefixtures()装饰器,它可以达到同样的效果。清单 5-6 中的代码是用这个装饰器实现的,它产生与清单 5-5 相同的输出。

import pytest

@pytest.fixture() def fixture01():
    print("\nIn fixture01()...")

@pytest.mark.usefixtures('fixture01')
def test_case01(fixture01):
    print("\nIn test_case01()...")

Listing 5-6test_module05.py

清单 5-6 的输出与清单 5-5 中的代码完全相同。

你可以为一个类使用@pytest.mark.usefixtures()装饰器,如清单 5-7 所示。

import pytest

@pytest.fixture()
def fixture01():
    print("\nIn fixture01()...")

@pytest.mark.usefixtures('fixture01')
class TestClass03:
   def test_case01(self):
      print("I'm the test_case01")

   def test_case02(self):
      print("I'm the test_case02")

Listing 5-7test_module06.py

以下是输出:

================== test session starts =======================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile: collected 2 items

test_module06.py::TestClass03::test_case01
In fixture01()...

I'm the test_case01
PASSED
test_module06.py::TestClass03::test_case02
In fixture01()...
I'm the test_case02
PASSED

================ 2 passed in 0.08 seconds ====================

如果您想在使用 fixture 的测试运行之后运行一段代码,您必须向 fixture 添加一个 finalizer 函数。清单 5-8 展示了这个想法。

import pytest

@pytest.fixture()
def fixture01(request):
    print("\nIn fixture...")

    def fin():
       print("\nFinalized...")
     request.addfinalizer(fin)

@pytest.mark.usefixtures('fixture01')
def test_case01():
    print("\nI'm the test_case01")

Listing 5-8test_module07.py

输出如下所示:

================= test session starts ========================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
 cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile: collected 1 items

test_module07.py::test_case01
In fixture...

I'm the test_case01
PASSED
Finalized...

============== 1 passed in 0.05 seconds =====================

pytest提供对所请求对象的夹具信息的访问。清单 5-9 展示了这个概念。

import pytest

@pytest.fixture()
def fixture01(request):
    print("\nIn fixture...")
    print("Fixture Scope: " + str(request.scope))
    print("Function Name: " + str(request.function. name ))
    print("Class Name: " + str(request.cls))
    print("Module Name: " + str(request.module. name ))
    print("File Path: " + str(request.fspath))

@pytest.mark.usefixtures('fixture01')
def test_case01():
    print("\nI'm the test_case01")

Listing 5-9test_module08.py

下面是清单 5-9 的输出:

================== test session starts =======================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
 cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile:
collected 1 items

test_module08.py::test_case01
In fixture...
Fixture Scope: function
Function Name: test_case01
Class Name: None
Module Name: test.test_module08
File Path: /home/pi/book/code/chapter05/test/test_module08.py

I'm the test_case01
PASSED

============== 1 passed in 0.06 seconds ===================

pytest 夹具的范围

pytest为你提供了一组范围变量来精确定义你想什么时候使用夹具。任何 fixture 的默认范围都是函数级。这意味着,默认情况下,固定设备处于功能级别。

以下显示了pytest夹具的范围列表:

  • function:每次测试运行一次

  • class:每类测试运行一次

  • module:每个模块运行一次

  • session:每个会话运行一次

要使用这些,请按如下方式定义它们:

  • 如果您想让 fixture 在每次测试后运行,请使用function范围。这对于较小的灯具来说很好。

  • 如果您希望 fixture 在每一类测试中运行,请使用class范围。通常,你会将相似的测试分组在一个类中,所以这可能是一个好主意,这取决于你如何组织事情。

  • 如果您想让 fixture 在当前文件开始时运行,然后在文件完成测试后运行,请使用module作用域。如果您有一个访问数据库的 fixture,并且您在模块开始时设置了数据库,然后终结器关闭了连接,那么这是一个好方法。

  • 如果您想在第一次测试时运行 fixture,并在最后一次测试运行后运行 finalizer,请使用session作用域。

@pytest.fixture(scope="class")

pytest中没有包的范围。然而,您可以通过确保只有特定的测试包在单个会话中运行,巧妙地将session范围用作包级范围。

pytest.raises()

unittest中,您有assertRaises()来检查是否有任何测试引发异常。在pytest也有类似的方法。它被实现为pytest.raises(),对于自动化负面测试场景非常有用。

考虑清单 5-10 中显示的代码。

import pytest

def test_case01():
    with pytest.raises(Exception):
        x = 1 / 0

def test_case02():
    with pytest.raises(Exception):
        x = 1 / 1

Listing 5-10test_module09.py

在清单 5-10 中,带有pytest.raises(Exception)的行检查代码中是否出现异常。如果在包含异常的代码块中引发了异常,则测试通过;否则,它会失败。

下面是清单 5-10 的输出:

============= test session starts =============================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter05/test,
inifile:
collected 2 items

test_module09.py::test_case01 PASSED
test_module09.py::test_case02 FAILED

=========================== FAILURES ==========================
__________________________test_case02__________________________
def test_case02():
   with pytest.raises(Exception):
>     x = 1 / 1
E     Failed: DID NOT RAISE <class 'Exception'>

test_module09.py:10: Failed
============== 1 failed, 1 passed in 0.21 seconds =============

test_case01()中,引发了一个异常,所以它通过了。test_case02()没有引发异常,所以失败。如前所述,这对于测试负面场景非常有用。

重要的 pytest 命令行选项

pytest 的一些更重要的命令行选项将在下面的部分中讨论。

帮助

如需帮助,请运行py.test -h。它将显示一个使用各种命令行选项的列表。

在第一次(或 N 次)失败后停止

您可以在第一次失败后使用py.test -x停止测试的执行。同样的,你可以使用py.test --maxfail=5在五次失败后停止执行。您也可以更改提供给--maxfail的参数。

分析测试执行持续时间

剖析意味着评估程序执行的时间、空间和内存等因素。分析主要是为了改进程序,使它们在执行时消耗更少的资源。你写的测试模块和套件基本上都是测试其他程序的程序。你可以用pytest找到最慢的测试。您可以使用py.test --durations=10命令来显示最慢的测试。您可以更改提供给--duration的参数。例如,尝试在chapter05目录上运行这个命令。

JUnit 风格的日志

像 JUnit(Java 的单元测试自动化框架)这样的框架以 XML 格式生成执行日志。您可以通过运行以下命令为您的测试生成 JUnit 风格的 XML 日志文件:

py.test --junitxml=result.xml

XML 文件将在当前目录中生成。

结论

以下是我使用pytest的原因,推荐所有 Python 爱好者和专业人士使用:

  • unittest要好。由此产生的代码更加简洁明了。

  • nose不同,pytest仍在积极开发中。

  • 它有很好的控制测试执行的特性。

  • 它可以生成 XML 结果,不需要额外的插件。

  • 它可以运行unittest测试。

  • 它有自己的一套先进的装置,本质上是模块化的。

如果您正在从事一个使用unittestnosedoctest作为 Python 测试框架的项目,我建议将您的测试迁移到pytest

六、Selenium 测试

在上一章中,您已经熟悉了一个单元测试框架,pytest。现在,您应该对使用pytest框架编写单元测试有些熟悉了。在这一章中,你将学习名为 Selenium 的 webdriver 框架。

Selenium 简介

Selenium 是一个 webdriver 框架。它用于浏览器自动化。这意味着您可以通过编程方式打开浏览器程序(或浏览器应用)。您手动执行的所有浏览器操作都可以通过 webdriver 框架以编程方式执行。Selenium 是用于浏览器自动化的最流行的 webdriver 框架。

Jason Huggins 于 2004 年在 ThoughtWorks 开发了 Selenium 作为工具。它旨在供组织内部使用。该工具流行起来后,许多人加入了它的开发,并被开源。此后,它作为开放源代码继续发展。哈金斯于 2007 年加入谷歌,并继续开发该工具。

名称 Selenium 是 Mercury Interactive 上开的一个玩笑,它也创造了测试自动化的专有工具。笑话是汞中毒可以用 Selenium 治愈,所以新的开源框架被命名为 Selenium。Selenium 和汞都是元素周期表中的元素。

ThoughtWorks 的 Simon Stewart 开发了一个叫做 WebDriver 的浏览器自动化工具。ThoughtWorks 和 Google 的开发人员在 2009 年的 Google 测试自动化会议上相遇,并决定合并 Selenium 和 Webdriver 项目。这个新框架被命名为 Selenium Webdriver 或 Selenium 2.0。

Selenium 有三个主要成分:

  • Selenium IDE

  • Selenium Webdriver

  • Selenium 栅

在本章中,你将会读到 Selenium IDE 和 Selenium Webdriver。

Selenium IDE

Selenium IDE 是一个用于记录浏览器动作的浏览器插件。录制后,您可以回放整个动作序列。您还可以将脚本操作导出为各种编程语言的代码文件。让我们从在 Chrome 和 Firefox 浏览器上安装插件开始。

使用以下 URL 将扩展添加到 Chrome web 浏览器:

https://chrome.google.com/webstore/detail/selenium-ide/mooikfkahbdckldjjndioackbalphokd

一旦它被添加,你可以从地址栏旁边的菜单中访问它,如图 6-1 所示。

img/436414_2_En_6_Fig1_HTML.jpg

图 6-1。

铬的 Selenium IDE

您可以从以下 URL 访问 Firefox 浏览器的附加组件:

https://addons.mozilla.org/en-GB/firefox/addon/selenium-ide/

添加后,可以从地址栏旁边的菜单中访问,如图 6-2 右上角所示。

img/436414_2_En_6_Fig2_HTML.jpg

图 6-2。

铬的 Selenium IDE

在各自的浏览器中点击这些选项会打开一个窗口,如图 6-3 所示。

img/436414_2_En_6_Fig3_HTML.jpg

图 6-3。

Selenium IDE 窗口

Selenium IDE 的 GUI 对于所有浏览器都是一样的。点击新建项目,打开新窗口,如图 6-4 所示。

img/436414_2_En_6_Fig4_HTML.jpg

图 6-4。

Selenium 新项目

输入您选择的名称。这将启用确定按钮。点击确定按钮,显示图 6-5 中的窗口。

img/436414_2_En_6_Fig5_HTML.jpg

图 6-5。

Selenium IDE 窗口

如您所见,该窗口分为多个部分。在左上方,您可以看到项目的名称。在右上角,有三个图标。单击第一个图标会创建一个新项目。第二个图标用于打开现有项目。第三个图标保存当前项目。保存的文件有一个*.side扩展名(Selenium IDE)。

让我们重命名现有的测试。检查左侧选项卡。可以看到一个未命名的测试,如图 6-6 所示。

img/436414_2_En_6_Fig6_HTML.jpg

图 6-6。

重命名未命名的测试

当您保存项目时,它会尝试将其保存为一个新文件。您必须通过覆盖先前的文件来用现有的名称保存它。现在,单击录制按钮。快捷键是 Ctrl+U,它会打开一个新的对话框,要求您输入项目的基本 URL 见图 6-7 。

img/436414_2_En_6_Fig7_HTML.jpg

图 6-7

项目基本 URL

你必须输入要测试的网页的网址。URL 还应该包含文本http://https://,否则不会将其视为 URL。在 http://www.google.com 输入框中输入。然后,它将启用开始录制按钮。录制按钮是红色的,位于窗口的右上角。点击按钮,它将启动一个新的窗口与指定的网址。看起来像图 6-8 。

img/436414_2_En_6_Fig8_HTML.png

图 6-8。

Selenium IDE 记录

在搜索栏中输入 Python ,然后点击谷歌搜索。它会向您显示搜索结果。单击第一个结果,然后在加载页面后,关闭浏览器窗口。然后单击菜单中的按钮停止录制。你会在 IDE 中看到记录的步骤,如图 6-9 所示。

img/436414_2_En_6_Fig9_HTML.jpg

图 6-9。

记录后的 Selenium IDE

您可以自动重新运行所有步骤。您可以在录制按钮的同一栏中看到一组四个图标。第一个图标用于运行所有测试,第二个图标用于运行当前测试。当前项目只有一个测试,因此它将运行套件中唯一的测试。单击任一按钮,自动重复这一系列操作。

这样你就可以记录和执行一系列的动作。一旦记录的测试成功执行,底部将显示日志,如图 6-10 所示。

img/436414_2_En_6_Fig10_HTML.jpg

图 6-10。

Selenium IDE 日志

您可以通过单击菜单中的+图标向项目中添加新的测试。一个项目通常会有多个测试。现在,您将学习如何导出项目。你可以右击测试打开菜单,如图 6-6 所示。单击导出选项。它打开一个新窗口,如图 6-11 所示。

img/436414_2_En_6_Fig11_HTML.jpg

图 6-11。

将项目导出为代码

选中顶部的两个选项,然后单击导出按钮。它将打开一个名为另存为的窗口。提供详细信息,它会将项目保存为 Python 文件,扩展名为*.py,保存在指定的目录中。生成的代码如清单 6-1 所示。

# Generated by Selenium IDE
import pytest
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

class TestTest01():
  def setup_method(self, method):
    self.driver = webdriver.Chrome()
    self.vars = {}

  def teardown_method(self, method):
    self.driver.quit()

  def test_test01(self):
    # Test name: Test01
    # Step # | name | target | value
    # 1 | open | / |
    self.driver.get("https://www.google.com/")
    # 2 | setWindowSize | 1042x554 |
    self.driver.set_window_size(1042, 554)
    # 3 | type | name=q | python
    self.driver.find_element(By.NAME, "q").send_keys("python")
    # 4 | click | css=form > div:nth-child(1) |
    self.driver.find_element(By.CSS_SELECTOR, "form > div:nth-child(1)").click()
    # 5 | click | css=center:nth-child(1) > .gNO89b |
    self.driver.find_element(By.CSS_SELECTOR, "center:nth-child(1) > .gNO89b").click()
    # 6 | click | css=.eKjLze .LC20lb |
    self.driver.find_element(By.CSS_SELECTOR, ".eKjLze .LC20lb").click()
    # 7 | close |  |
    self.driver.close()

Listing 6-1test_test01.py

这就是如何将自动化测试导出到 Python 的方法。您可以使用unittest框架运行这个文件,以便稍后重现测试。暂时不要执行代码,因为您还没有为 Python 安装 Selenium 框架。在下一节中,您将分析并学习编写自己的代码。

Selenium Webdriver

Selenium IDE 是一个插件。它只是一个记录和回放工具,带有一点定制测试用例的条款。如果你想完全控制你的测试,你应该能够从头开始写。Selenium Webdriver 允许您这样做。

上一节中导出的代码使用 webdriver 实现浏览器自动化。在这里,您将看到如何从头开始编写自己的代码。您可以使用以下命令安装 Selenium Webdriver:

pip3 install selenium

现在,您可以运行上一节中保存的代码。

让我们看看如何从头开始编写代码。查看清单 6-2 中的代码。

from selenium import webdriver
driver_path=r'D:\\drivers\\geckodriver.exe'
driver = webdriver.Firefox(executable_path=driver_path)
driver.close()

Listing 6-2prog00.py

请逐行考虑这段代码。第一行将库导入到程序中。第二行定义了一个字符串。该字符串包含您要自动化的浏览器的驱动程序可执行文件的路径。您可以从以下 URL 下载各种浏览器的驱动程序:

https://sites.google.com/chromium.org/driver/

https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

https://github.com/mozilla/geckodriver/releases

访问这些网页并下载适合您的操作系统(Windows/Linux/macOS)和体系结构(32/64 位)组合的驱动程序。我将它们下载并保存在 Windows 64 位操作系统上由D:\drivers标识的位置。

第三行创建一个驱动对象,第四行关闭它。从空闲或命令行启动程序。它将立即打开和关闭浏览器。如果你使用 IDLE,它也会单独打开geckodriver.exe文件,你必须手动关闭它。您将很快看到如何以编程方式终止它。现在,手动关闭它。查看列表 6-3 。

from selenium import webdriver
driver_path=r'D:\\drivers\\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
driver.close()
driver.quit()

Listing 6-3prog01.py

这里,你正在使用 Chrome 驱动程序,并在最后一行关闭驱动程序可执行文件。运行这个程序来查看代码的运行情况。接下来,让我们试验一下 edge 浏览器,并在代码中添加一些等待时间。查看清单 6-4 。

from selenium import webdriver
import time
driver_path=r'D:\\drivers\\msedgedriver.exe'
driver = webdriver.Edge(executable_path=driver_path)
time.sleep(10)
driver.close()
time.sleep(5)
driver.quit()

Listing 6-4prog02.py

运行代码以查看它的运行情况。

你也可以为 Safari 浏览器编写代码。Safari webdriver 预装在 macOS 中。可以在/usr/bin/safaridriver找到。您可以使用以下 shell 命令来启用它:

safaridriver –enable

您可以使用 Python 中的以下代码行创建驱动程序对象:

driver = webdriver.Safari()

Selenium 与单位测试

可以用 Selenium 框架搭配unittest。这样,您可以为不同的情况创建不同的测试。您可以通过这种方式跟踪测试的进展。参见清单 6-5 。

import unittest
from selenium import webdriver

class TestClass01(unittest.TestCase):

    def setUp(self):
        driver_path=r'D:\\drivers\\geckodriver.exe'
        driver = webdriver.Firefox(executable_path=driver_path)
        self.driver = driver
        print ("\nIn setUp()...")

    def tearDown(self):
        print ("\nIn tearDown")
        self.driver.close()
        self.driver.quit()

    def test_case01(self):
        print("\nIn test_case01()...")
        self.driver.get("http://www.python.org")
        assert self.driver.title == "Welcome to Python.org"

if __name__ == "__main__":
    unittest.main()

Listing 6-5test_test02.py

这个脚本创建 webdriver 对象,打开一个网页并检查其标题,完成后,它关闭浏览器窗口和 webdriver。运行脚本来看看它的运行情况。

结论

在本章中,您学习了使用 Selenium 实现 web 浏览器自动化的基础知识。您还了解了 Selenium IDE 以及如何将unittest与 Selenium 结合起来。

下一章专门讨论 Python 中的日志机制。

七、在 Python 中记录日志

在上一章中,您已经熟悉了单元测试框架 Selenium。这一章改变了节奏,您将学习一个相关的主题,日志记录。

本章包括以下内容:

  • 日志记录基础

  • 使用操作系统记录日志

  • 手动记录文件操作

  • 在 Python 中登录

  • loguru记录

读完这一章后,你会更加适应用 Python 登录。

日志记录基础

记录某事的过程被称为记录。例如,如果我正在记录温度,这就是所谓的温度记录,这是物理记录的一个例子。我们也可以在计算机编程中使用这个概念。很多时候,你会在终端上得到一个中间输出。它用于在程序运行时进行调试。有时程序会使用crontab(在 UNIX 类操作系统中)或使用 Windows 调度程序自动运行。在这种情况下,日志记录用于确定执行过程中是否存在问题。通常,此类信息会记录到文件中,这样,如果维护或操作人员不在场,他们可以在最早的可用时间查看日志。有多种方法可以记录与程序执行相关的信息。下面几节逐一看。

使用操作系统记录日志

让我们使用命令行登录操作系统。考虑清单 7-1 中的程序。

import datetime
import sys
print("Commencing Execution of the program...")
print(datetime.datetime.now())
for i in [1, 2, 3, 4, 5]:
    print("Iteration " + str(i) + " ...")
print("Done...")
print(datetime.datetime.now())
sys.exit(0)

Listing 7-1prog00.py

当您使用 IDLE 或任何 IDE 运行此命令时,您将在终端中看到以下输出:

Commencing Execution of the program...
2021-09-01 19:09:14.900123
Iteration 1 ...
Iteration 2 ...
Iteration 3 ...
Iteration 4 ...
Iteration 5 ...
Done...
2021-09-01 19:09:14.901121

这就是在终端上登录的样子。您也可以将此记录在文件中。您可以在 Linux 和 Windows 中使用 IO 重定向来实现这一点。您可以在 Windows 命令提示符下运行该程序,如下所示:

python prog00.py >> test.log

在 Linux 终端上,命令如下:

python3 prog00.py >> test.log

这个命令将在同一个目录中创建一个名为test.log的新文件,并将所有输出重定向到那里。

这是显示执行日志并将其保存在文件中的方式。

手动记录文件操作

本节解释如何用 Python 记录文件操作事件。首先你需要打开一个文件。使用open()程序来完成。在 Python 3 解释器提示符下运行以下示例:

>>> logfile = open('mylog.log', 'w')

该命令为文件操作创建一个名为logfile的对象。open()例程的第一个参数是文件名,第二个操作是打开文件的模式。这个例子使用了代表写操作的w模式。有许多打开文件的模式,但这是目前唯一相关的模式。作为练习,你可以探索其他模式。

如果文件存在,前面的代码行以写模式打开该文件;否则,它会创建一个新文件。现在运行以下代码:

>>> logfile.write('This is the test log.')

输出如下所示:

21

write()例程将给定的字符串写入文件,并返回字符串的长度。最后,您可以关闭 file 对象,如下所示:

>>> logfile.close()

现在,让我们修改前面的脚本prog00.py来添加日志文件操作,如清单 7-2 所示。

import datetime
import sys
logfile = open('mylog.log', 'w')
msg = "Commencing Execution of the program...\n" + str(datetime.datetime.now())
print(msg)
logfile.write(msg)
for i in [1, 2, 3, 4, 5]:
    msg = "\nIteration " + str(i) + " ..."
    print(msg)
    logfile.write(msg)
msg = "\nDone...\n" + str(datetime.datetime.now())
logfile.write(msg)
print(msg)
logfile.close()
sys.exit(0)

Listing 7-2prog01.py

正如您在清单 7-2 中看到的,您正在创建日志消息的字符串。然后,程序将它们同时发送到日志文件和终端。您可以使用空闲或命令提示符运行该程序。

这就是如何使用文件操作手动记录程序的执行。

在 Python 中登录

本节解释 Python 中的日志记录过程。您不需要为此安装任何东西,因为它是 Python 安装的一部分,是包含电池的哲学的一部分。您可以按如下方式导入日志库:

import logging

在进入编程部分之前,您需要了解一些重要的东西——日志记录的级别。日志记录有五个级别。这些级别具有指定的优先级。以下是这些级别的列表,按严重程度的升序排列:

DEBUG
INFO
WARNING
ERROR
CRITICAL

现在考虑清单 7-3 中的代码示例。

import logging
logging.debug('Debug')
logging.info('Info')
logging.warning('Warning')
logging.error('Error')
logging.critical('Critical')

Listing 7-3prog02.py

输出如下所示:

WARNING:root:Warning
ERROR:root:Error
CRITICAL:root:Critical

如您所见,只打印了最后三行日志。这是因为日志记录的默认级别是Warning。这意味着从warning开始的所有记录级别都将被记录。其中包括WarningErrorCritical

您可以更改日志记录的级别,如清单 7-4 所示。

import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('Debug')
logging.info('Info')
logging.warning('Warning')
logging.error('Error')
logging.critical('Critical')

Listing 7-4prog03.py

如您所见,basicConfig()例程配置了日志记录的级别。在调用任何日志例程之前,您需要调用这个例程。该示例将日志记录级别设置为DebugDebug是最低级别的日志记录,这意味着所有日志记录级别为Debug及以上的日志都将被记录。输出如下所示:

DEBUG:root:Debug
INFO:root:Info
WARNING:root:Warning
ERROR:root:Error
CRITICAL:root:Critical

让我们详细看看日志消息。如您所见,日志消息分为三部分。第一部分是日志的级别。第二部分是记录器的名称。在这种情况下,它是根日志记录器。第三部分是传递给日志例程的字符串。稍后您将了解如何更改此消息的详细信息。

这是讨论不同日志记录级别的含义的好时机。DebugInfo级别通常表示程序的一般执行。Warning日志记录级别表明问题并不严重。Error当你有严重问题影响程序正常执行时使用。最后,Critical是最高级别,它表示系统范围的故障。

记录到文件

您已经学习了如何在终端上显示日志消息。您还可以将消息记录到一个文件中,如清单 7-5 所示。

import logging
logging.basicConfig(filename='logfile.log',
                    encoding='utf-8',
                    level=logging.DEBUG)
logging.debug('Debug')
logging.info('Info')
logging.warning('Warning')
logging.error('Error')
logging.critical('Critical')

Listing 7-5prog04.py

如您所见,该程序设置了日志文件的编码和名称。运行程序并检查日志文件。

该程序检查日志文件是否存在,其名称作为字符串传递给basicConfig()例程。如果文件不存在,它将创建名为的文件。否则,它将追加到现有文件中。如果您想在每次执行代码时创建一个新文件,您可以使用清单 7-6 中的代码来实现。

import logging
logging.basicConfig(filename='logfile.log',
                    encoding='utf-8',
                    filemode='w',
                    level=logging.DEBUG)
logging.debug('Debug')
logging.info('Info')
logging.warning('Warning')
logging.error('Error')
logging.critical('Critical')

Listing 7-6prog05.py

注意调用basicConfig()例程的附加参数和相关参数。

自定义日志消息

您也可以自定义日志消息。您必须通过向basicConfig()例程的参数传递一个参数来指定这一点。清单 7-7 给出了一个例子。

import logging
logging.basicConfig(filename='logfile.log',
                    format='%(asctime)s:%(levelname)s:%(message)s',
                    encoding='utf-8',
                    filemode='w',
                    level=logging.DEBUG)
logging.debug('Debug')
logging.info('Info')
logging.warning('Warning')
logging.error('Error')
logging.critical('Critical')

Listing 7-7prog06.py

正如您所看到的,这个例子将格式化字符串'%(asctime)s:%(levelname)s:%(message)s'传递给了basicConfig()例程的参数format。输出如下所示:

2021-09-02 13:36:35,401:DEBUG:Debug
2021-09-02 13:36:35,401:INFO:Info
2021-09-02 13:36:35,401:WARNING:Warning
2021-09-02 13:36:35,401:ERROR:Error
2021-09-02 13:36:35,401:CRITICAL:Critical

输出显示日期和时间、日志记录级别和消息。

自定义日志记录操作

到目前为止,示例一直使用默认的记录器,称为root。您也可以创建自己的自定义记录器。记录器对象向处理程序对象发送日志消息。处理程序将日志消息发送到它们的目的地。目标可以是日志文件或控制台。您可以为控制台处理程序和文件处理程序创建对象。日志格式化程序用于格式化日志消息的内容。让我们一行一行地看一个例子。创建一个名为prog07.py的新文件。现在,您将看到如何将代码添加到该文件中,以显示定制的日志记录操作。

按如下方式导入库:

import logging

创建自定义记录器,如下所示:

logger = logging.getLogger('myLogger')
logger.setLevel(logging.DEBUG)

您已经创建了名为myLogger的定制记录器。每当您在日志消息中包含该名称时,它将显示myLogger而不是root。现在创建一个处理程序来记录文件。

fh = logging.FileHandler('mylog.log', encoding='utf-8')
fh.setLevel(logging.DEBUG)

创建文件格式化程序:

file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

将其设置为文件处理程序:

fh.setFormatter(file_formatter)

将文件处理程序添加到记录器:

logger.addHandler(fh)

您也可以创建一个控制台处理程序。对新的控制台处理程序重复这些步骤:

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger.addHandler(ch)

整个脚本如清单 7-8 所示。

import logging
logger = logging.getLogger('myLogger')
logger.setLevel(logging.DEBUG)

fh = logging.FileHandler('mylog.log',
                         encoding='utf-8')
fh.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(file_formatter)
logger.addHandler(fh)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger.addHandler(ch)
logger.debug('Debug')
logger.info('Info')
logger.warning('Warning')
logger.error('Error')
logger.critical('Critical')

Listing 7-8prog07.py

这是您可以同时登录到控制台和文件的方式。运行代码并查看输出。

旋转日志文件

您还可以循环使用日志文件。你只需要修改清单 7-8 中的一行。循环日志文件意味着所有新日志将被写入新文件,旧日志将通过重命名日志文件来备份。查看列表 7-9 。

import logging
import logging.handlers
logfile = 'mylog.log'
logger = logging.getLogger('myLogger')
logger.setLevel(logging.DEBUG)
rfh = logging.handlers.RotatingFileHandler(logfile,
                                           maxBytes=10,
                                           backupCount=5)
rfh.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
rfh.setFormatter(file_formatter)
logger.addHandler(rfh)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger.addHandler(ch)
logger.debug('Debug')
logger.info('Info')
logger.warning('Warning')
logger.error('Error')
logger.critical('Critical')

Listing 7-9prog08.py

正如您在清单 7-9 中看到的,代码已经实现了旋转文件句柄。下面一行代码创建了它:

rfh = logging.handlers.RotatingFileHandler(logfile,
                                           maxBytes=10,
                                           backupCount=5)

它按如下方式创建日志文件:

mylog.log
mylog.log.1
mylog.log.2
mylog.log.3
mylog.log.4
mylog.log.5

最近的日志保存在mylog.log中,容量为 10 字节。当该日志文件达到 10 字节时,如例程调用参数maxBytes中所指定的,它被重命名为mylog.log.1。当文件再次充满时,重复该过程,并且mylog.log.2被重命名为mylog.log.2。该过程继续,从mylog.log.5开始的文件被清除。这是因为您将5作为参数传递给了backupCount参数。作为练习,尝试改变参数。

使用多个记录器

你也可以在你的程序中使用多个记录器。清单 7-10 创建了两个记录器、一个处理程序和一个格式化程序。该处理程序在记录器之间共享。

import logging
logger1 = logging.getLogger('Logger1')
logger1.setLevel(logging.DEBUG)
logger2 = logging.getLogger('Logger2')
logger2.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger1.addHandler(ch)
logger2.addHandler(ch)
logger1.debug('Debug')
logger2.debug('Debug')
logger1.info('Info')
logger2.info('Info')
logger1.warning('Warning')
logger2.warning('Warning')
logger1.error('Error')
logger2.error('Error')
logger1.critical('Critical')
logger2.critical('Critical')

Listing 7-10prog09.py

输出如下所示:

2021-09-03 00:25:40,135:Logger1:DEBUG:Debug
2021-09-03 00:25:40,153:Logger2:DEBUG:Debug
2021-09-03 00:25:40,161:Logger1:INFO:Info
2021-09-03 00:25:40,168:Logger2:INFO:Info
2021-09-03 00:25:40,176:Logger1:WARNING:Warning
2021-09-03 00:25:40,184:Logger2:WARNING:Warning
2021-09-03 00:25:40,193:Logger1:ERROR:Error
2021-09-03 00:25:40,200:Logger2:ERROR:Error
2021-09-03 00:25:40,224:Logger1:CRITICAL:Critical
2021-09-03 00:25:40,238:Logger2:CRITICAL:Critical

现在,您将看到如何为两个记录器创建单独的处理程序和格式化程序。清单 7-11 显示了一个例子。

import logging
logger1 = logging.getLogger('Logger1')
logger1.setLevel(logging.DEBUG)
logger2 = logging.getLogger('Logger2')
logger2.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger1.addHandler(ch)
fh = logging.FileHandler('mylog.log',
                         encoding='utf-8')
fh.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(file_formatter)
logger2.addHandler(fh)
logger1.debug('Debug')
logger2.debug('Debug')
logger1.info('Info')
logger2.info('Info')
logger1.warning('Warning')
logger2.warning('Warning')
logger1.error('Error')
logger2.error('Error')
logger1.critical('Critical')
logger2.critical('Critical')

Listing 7-11prog10.py

如您所见,有两组独立的记录器、处理程序和格式化程序。一组将日志发送到控制台,另一组将日志发送到日志文件。控制台的输出如下:

2021-09-03 15:13:37,513:Logger1:DEBUG:Debug
2021-09-03 15:13:37,533:Logger1:INFO:Info
2021-09-03 15:13:37,542:Logger1:WARNING:Warning
2021-09-03 15:13:37,552:Logger1:ERROR:Error
2021-09-03 15:13:37,560:Logger1:CRITICAL:Critical

日志文件的输出如下:

2021-09-03 15:13:37,532 - Logger2 - DEBUG - Debug
2021-09-03 15:13:37,542 - Logger2 - INFO - Info
2021-09-03 15:13:37,551 - Logger2 - WARNING - Warning
2021-09-03 15:13:37,560 - Logger2 - ERROR - Error
2021-09-03 15:13:37,569 - Logger2 - CRITICAL – Critical

用线程记录日志

有时,你会在你的程序中使用多线程。Python 允许对线程使用日志记录功能。这可以确保您了解程序中使用的线程的执行细节。创建一个新的 Python 文件,将其命名为prog11.py。将以下代码添加到该文件中:

import logging
import threading
import time

现在创建一个函数,如下所示:

def worker(arg, number):
    while not arg['stop']:
        logging.debug('Hello from worker() thread number '
                      + str(number))
        time.sleep(0.75 * number)

这个函数接受一个参数,除非您终止它,否则它会一直运行一个显示消息的循环。

让我们按如下方式配置默认控制台记录器:

logging.basicConfig(level='DEBUG',                    format='%(asctime)s:%(name)s:%(levelname)s:%(message)s')

现在创建两个线程,如下所示:

info = {'stop': False}
thread1 = threading.Thread(target=worker, args=(info, 1, ))
thread1.start()
thread2 = threading.Thread(target=worker, args=(info, 2, ))
thread2.start()

创建一个将被键盘中断的循环,同时也会中断线程:

while True:
    try:
        logging.debug('Hello from the main() thread')
        time.sleep(1)
    except KeyboardInterrupt:
        info['stop'] = True
        break

最后,连接这些线程:

thread1.join()
thread2.join()

整个程序如清单 7-12 所示。

import logging
import threading
import time
def worker(arg, number):
    while not arg['stop']:
        logging.debug('Hello from worker() thread number '
                      + str(number))
        time.sleep(0.75 * number)
logging.basicConfig(level='DEBUG',
format='%(asctime)s:%(name)s:%(levelname)s:%(message)s')
info = {'stop': False}
thread1 = threading.Thread(target=worker, args=(info, 1, ))
thread1.start()
thread2 = threading.Thread(target=worker, args=(info, 2, ))
thread2.start()
while True:
    try:
        logging.debug('Hello from the main() thread')
        time.sleep(1)
    except KeyboardInterrupt:
        info['stop'] = True
        break
thread1.join()
thread2.join()

Listing 7-12prog11.py

运行程序,几秒钟后按 Ctrl+C 终止程序。输出如下所示:

2021-09-03 15:34:27,071:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:27,304:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:27,664:root:DEBUG:Hello from worker() thread number 2
2021-09-03 15:34:27,851:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:28,364:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:28,629:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:29,239:root:DEBUG:Hello from worker() thread number 2
2021-09-03 15:34:29,381:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:29,414:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:30,205:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:30,444:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:30,788:root:DEBUG:Hello from worker() thread number 2
2021-09-03 15:34:30,990:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:31,503:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:31,828:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:32,311:root:DEBUG:Hello from worker() thread number 2
2021-09-03 15:34:32,574:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:32,606:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:33,400:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:33,634:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:33,865:root:DEBUG:Hello from worker() thread number 2
2021-09-03 15:34:34,175:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:34,688:root:DEBUG:Hello from the main() thread
2021-09-03 15:34:34,969:root:DEBUG:Hello from worker() thread number 1
2021-09-03 15:34:35,456:root:DEBUG:Hello from worker() thread number 2
Traceback (most recent call last):
  File "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition/Code/Chapter07/prog11.py", line 26, in <module>
    thread2.join()
KeyboardInterrupt

多个记录器写入同一个目标

您可以让多个记录器写入同一个目标。清单 7-13 中所示的代码示例将两个不同记录器的日志发送到一个控制台处理程序和一个文件处理程序。

import logging
logger1 = logging.getLogger('Logger1')
logger1.setLevel(logging.DEBUG)
logger2 = logging.getLogger('Logger2')
logger2.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
console_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
ch.setFormatter(console_formatter)
logger1.addHandler(ch)
logger2.addHandler(ch)
fh = logging.FileHandler('mylog.log',
                         encoding='utf-8')
fh.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(file_formatter)
logger1.addHandler(fh)
logger2.addHandler(fh)
logger1.debug('Debug')
logger2.debug('Debug')
logger1.info('Info')
logger2.info('Info')
logger1.warning('Warning')
logger2.warning('Warning')
logger1.error('Error')
logger2.error('Error')
logger1.critical('Critical')
logger2.critical('Critical')

Listing 7-13prog12.py

运行该程序,在控制台上查看以下输出:

2021-09-03 16:10:53,938:Logger1:DEBUG:Debug
2021-09-03 16:10:53,956:Logger2:DEBUG:Debug
2021-09-03 16:10:53,966:Logger1:INFO:Info
2021-09-03 16:10:53,974:Logger2:INFO:Info
2021-09-03 16:10:53,983:Logger1:WARNING:Warning
2021-09-03 16:10:53,993:Logger2:WARNING:Warning
2021-09-03 16:10:54,002:Logger1:ERROR:Error
2021-09-03 16:10:54,011:Logger2:ERROR:Error
2021-09-03 16:10:54,031:Logger1:CRITICAL:Critical
2021-09-03 16:10:54,049:Logger2:CRITICAL:Critical

日志文件包含程序执行后的以下日志:

2021-09-03 16:10:53,938 - Logger1 - DEBUG - Debug
2021-09-03 16:10:53,956 - Logger2 - DEBUG - Debug
2021-09-03 16:10:53,966 - Logger1 - INFO - Info
2021-09-03 16:10:53,974 - Logger2 - INFO - Info
2021-09-03 16:10:53,983 - Logger1 - WARNING - Warning
2021-09-03 16:10:53,993 - Logger2 - WARNING - Warning
2021-09-03 16:10:54,002 - Logger1 - ERROR - Error
2021-09-03 16:10:54,011 - Logger2 - ERROR - Error
2021-09-03 16:10:54,031 - Logger1 - CRITICAL - Critical
2021-09-03 16:10:54,049 - Logger2 - CRITICAL – Critical

使用 loguru 记录日志

Python 还有另一种可以安装和使用的日志记录机制。它被称为loguru。是第三方库,需要单独安装。它比内置的日志记录器略好,并且有更多的功能。在本节中,您将看到如何安装、使用和探索它。

您可以使用以下命令在 Windows 和 Linux 上安装loguru:

pip3 install loguru

以下是 Windows 计算机上的安装日志:

Collecting loguru
  Downloading loguru-0.5.3-py3-none-any.whl (57 kB)
     |████████████████| 57 kB 1.1 MB/s
Collecting win32-setctime>=1.0.0
  Downloading win32_setctime-1.0.3-py3-none-any.whl (3.5 kB)
Requirement already satisfied: colorama>=0.3.4 in c:\users\ashwin\appdata\local\programs\python\python39\lib\site-packages (from loguru) (0.4.4)
Installing collected packages: win32-setctime, loguru
Successfully installed loguru-0.5.3 win32-setctime-1.0.3

使用 loguru 和可用的日志记录级别

loguru只有一个记录者。您可以根据需要进行配置。默认情况下,它会将日志消息发送给stderr。清单 7-14 显示了一个简单的例子。

from loguru import logger
logger.trace('Trace')
logger.debug('Debug')
logger.info('Info')
logger.success('Success')
logger.warning('Warning')
logger.error('Error')
logger.critical('Critical')

Listing 7-14Prog13.py

清单 7-14 中的代码按照严重性的升序列出了所有日志记录级别。您还可以将所有事情记录到一个文件中,如清单 7-15 所示。

from loguru import logger
import sys
logger.add("mylog_{time}.log",
           format="{time}:{level}:{message}",
           level="TRACE")
logger.trace('Trace')
logger.debug('Debug')
logger.info('Info')
logger.success('Success')
logger.warning('Warning')
logger.error('Error')
logger.critical('Critical')

Listing 7-15Prog14.py

运行此命令时,文件的输出如下:

2021-09-02T21:56:04.677854+0530:TRACE:Trace
2021-09-02T21:56:04.680839+0530:DEBUG:Debug
2021-09-02T21:56:04.706743+0530:INFO:Info
2021-09-02T21:56:04.726689+0530:SUCCESS:Success
2021-09-02T21:56:04.749656+0530:WARNING:Warning
2021-09-02T21:56:04.778333+0530:ERROR:Error
2021-09-02T21:56:04.802271+0530:CRITICAL:Critical

您还可以创建一个定制的日志级别,如清单 7-16 所示。

from loguru import logger
import sys
logger.add("mylog_{time}.log",
           format="{time}:{level}:{message}",
           level="TRACE")
new_level = logger.level("OKAY", no=15, color="<green>")
logger.trace('Trace')
logger.debug('Debug')
logger.log("OKAY", "All is OK!")
logger.info('Info')

Listing 7-16Prog15.py

这段代码用logger.level()例程创建了一个新的级别。可以配合logger.log()例程使用。运行程序。转储到日志文件中的输出如下:

2021-09-02T22:44:59.834885+0530:TRACE:Trace
2021-09-02T22:44:59.839871+0530:DEBUG:Debug
2021-09-02T22:44:59.893727+0530:OKAY:All is OK!
2021-09-02T22:44:59.945590+0530:INFO:Info

自定义文件保留

日志文件就像任何其他信息一样,需要存储空间。随着时间的推移和多次执行,日志文件会变得越来越大。如今,存储更便宜了。尽管如此,空间总是有限的,存储旧的和不必要的日志是对空间的浪费。许多组织都制定了保留旧日志的策略。您可以通过以下方式实现这些策略。

以下配置旋转大文件。您可以按如下方式指定文件的大小:

logger.add("mylog_{time}.log", rotation="2 MB")

以下配置在午夜后创建一个新文件:

logger.add("mylog_{time}.log", rotation="00:01")

以下配置会循环一周前的文件:

logger.add("mylog_{time}.log", rotation="1 week")

以下配置在指定的天数后清理文件:

logger.add("mylog_{time}.log", retention="5 days")  # Cleanup after some time

以下配置将文件压缩为 ZIP 格式:

logger.add("mylog_{time}.log", compression="zip")

作为练习,尝试所有这些配置。

自定义跟踪

您可以自定义跟踪过程,并获取有关任何潜在问题的详细信息。在内置的记录器中实现这一点很困难,但是使用loguru可以很容易地做到。您可以通过传递一些额外的参数来自定义跟踪,如清单 7-17 所示。

from loguru import logger

logger.add('mylog.log',
           backtrace=True,
           diagnose=True)

def function1(a, b):
    return a / b

def function2(c):
    try:
        function1(5, c)
    except ZeroDivisionError:
        logger.exception('Divide by Zero!')

function2(0)

Listing 7-17Prog16.py

这些附加参数允许您详细跟踪故障。日志文件有以下输出:

2021-09-03 17:16:40.122 | ERROR    | __main__:function2:14 - Divide by Zero!
Traceback (most recent call last):

  File "<string>", line 1, in <module>

  File "C:\Users\Ashwin\AppData\Local\Programs\Python\Python39\lib\idlelib\run.py", line 156, in main
    ret = method(*args, **kwargs)
          |       |       -> {}
          |       -> (<code object <module> at 0x000001D3E9EFDB30, file "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition...
          -> <bound method Executive.runcode of <idlelib.run.Executive object at 0x000001D3E802F730>>

  File "C:\Users\Ashwin\AppData\Local\Programs\Python\Python39\lib\idlelib\run.py", line 559, in runcode
    exec(code, self.locals)
         |     |    -> {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__...
         |     -> <idlelib.run.Executive object at 0x000001D3E802F730>
         -> <code object <module> at 0x000001D3E9EFDB30, file "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition/...

  File "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition/Code/Chapter07/prog16.py", line 16, in <module>
    function2(0)
    -> <function function2 at 0x000001D3EA6264C0>

> File "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition/Code/Chapter07/prog16.py", line 12, in function2
    function1(5, c)
    |            -> 0
    -> <function function1 at 0x000001D3EA61DE50>

  File "C:/Users/Ashwin/Google Drive/Python Unit Test Automation - Second Edition/Code/Chapter07/prog16.py", line 8, in function1
    return a / b
           |   -> 0
           -> 5

ZeroDivisionError: division by zero

自定义日志消息格式和显示

您还可以定制日志消息格式,并确定它在控制台上的显示方式,如清单 7-18 所示。

from loguru import logger
import sys
logger.add(sys.stdout,
           colorize=True,
           format="<blue>{time}</blue> <level>{message}</level>")
logger.add('mylog.log',
           format="{time:YYYY-MM-DD @ HH:mm:ss} - {level} - {message}")
logger.debug('Debug')
logger.info('Info')

Listing 7-18Prog17.py

如果您在控制台上运行这个程序,您将得到如图 7-1 所示的输出。

img/436414_2_En_7_Fig1_HTML.jpg

图 7-1

定制输出

使用字典进行配置

您也可以用字典配置日志文件,如清单 7-19 所示。

from loguru import logger
import sys
config = {
    'handlers': [
        {'sink': sys.stdout, 'format': '{time} - {message}'},
        {'sink': 'mylog.log', 'serialize': True}]
}
logger.configure(**config)
logger.debug('Debug')
logger.info('Info')

Listing 7-19Prog18.py

运行该程序,您将看到以下输出:

2021-09-03T17:44:49.396318+0530 - Debug
2021-09-03T17:44:49.416051+0530 – Info

结论

本章详细解释了 Python 的日志机制。日志记录是一种非常有用的技术,用于分析程序执行过程中遇到的问题。每个应用或程序都有独特的日志记录要求,您可以在日志文件中包含各种详细信息。本章介绍了几个日志记录的例子。作为练习,确定您希望在 Python 程序的日志中看到哪种信息,然后编写适当的日志代码。

下一章是你在本书中学到的所有东西的顶点。你学习了 TDD(测试驱动开发)。

八、提示和技巧

在本书的第一章,你大概了解了 Python 的历史和哲学。后续章节探讨了 Python 中各种测试自动化框架的特性。你探索的框架有doctestunittestnosenose2pytest。后来,您详细了解了 Selenium 和日志记录。本章着眼于编码约定,这些编码约定将使得跨这些框架的测试发现更加容易。然后,您将看到测试驱动开发的概念,并了解如何在pytest的帮助下在 Python 3 项目中实现它。

便于测试发现的编码和文件命名约定

您已经看到所有的xUnit风格的框架都包括测试发现,也就是测试的自动检测、执行和报告生成。这是一个非常重要的特性,因为它让代码测试人员的生活更加轻松。您甚至可以使用 OS 调度程序(例如,基于 Linux 的操作系统中的cron和 Windows 中的 Windows Scheduler)来调度测试发现过程,它们将在调度的时间自动运行测试。

为了确保测试发现系统成功地检测到所有测试,我通常遵循以下代码和文件名约定:

  • 所有测试模块(测试文件)的名称应该以test_开头

  • 所有测试函数的名称都应以test_开头

  • 所有测试类的名称都应该以Test开头

  • 所有测试方法的名称都应以test_开头

  • 将所有测试分组到测试类和包中

  • 所有包含测试代码的包都应该有一个init.py文件

遵循 PEP 8 的代码惯例总是一个好主意。可见于 https://www.python.org/dev/peps/pep-0008/

如果您对您的代码和文件名使用这些约定,所有测试自动化框架的测试发现特性——包括unittestnosenose2pytest——将毫无问题地检测测试。所以,下一次你写测试的时候,遵循这些惯例来获得最好的结果。

Note

你可以在 https://www.martinfowler.com/bliki/Xunit.htmlhttp://xunitpatterns.com/ 了解更多关于xUnit

使用 pytest 进行测试驱动的开发

测试驱动开发(TDD)是一种范式,通过这种范式,您首先编写测试,观察它们失败,然后编写代码使失败的测试通过,从而实现新的特性或需求。一旦以这种方式实现了基本框架,您就可以在此基础上通过修改测试和更改开发代码来适应添加的功能。您可以根据需要多次重复这个过程,以适应所有新的需求。

本质上,TDD 是一个循环,在这个循环中,您首先编写测试,看着它们失败,实现所需的特性,并重复这个过程,直到新的特性被添加到现有的代码中。

通过在开发代码之前编写自动化测试,它迫使你首先考虑手头的问题。当您开始构建您的测试时,您必须考虑您编写开发代码的方式,这些代码必须通过已经编写好的自动化测试才能被接受。

图 8-1 总结了 TDD 方法。

img/436414_2_En_8_Fig1_HTML.png

图 8-1

TDD 流程

要查看 TDD 是如何用pytest在 Python 中实现的,在code目录中为这个 TDD 创建一个名为chapter08的目录。您将在 TDD 练习中使用这个目录。

chapter08目录中创建清单 8-1 所示的测试模块。

class TestClass01:

   def test_case01(self):
      calc = Calculator()
      result = calc.add(2, 2)
       assert 4 == result

Listing 8-1test_module01.py

使用以下命令运行清单 8-1 中的代码:

py.test -vs test_module01.py

输出如下所示:

===================== test session starts =====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 -- / usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter08,
inifile:
collected 1 items

test_module01.py::TestClass01::test_case01 FAILED
=========================== FAILURES ==========================
____________________ TestClass01.test_case01___________________

self = <test_module01.TestClass01 object at 0x763c03b0>

   def test_case01(self):
>      calc = Calculator()
E      NameError: name 'Calculator' is not defined

test_module01.py:4: NameError
==================== 1 failed in 0.29 seconds =================

从这个输出可以看出问题在于Calculator没有被导入。那是因为你还没有创建Calculator模块!所以让我们在同一个目录下的一个名为calculator.py的文件中定义Calculator模块,如清单 8-2 所示。

class Calculator:

   def add(self, x, y):
       pass

Listing 8-2calculator.py

每次修改模块时,通过运行以下命令,确保calculator.py中没有错误:

python3 calculator.py

现在在测试模块中导入Calculator,如清单 8-3 所示。

from calculator import Calculator class TestClass01:
def test_case01(self):
calc = Calculator() result = calc.add(2, 2) assert 4 == result

Listing 8-3test_module01.py

再次运行test_module01.py。输出如下所示:

===================== test session starts =====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter08,
inifile:
collected 1 items

test_module01.py::TestClass01::test_case01 FAILED
========================= FAILURES ============================
___________________ TestClass01.test_case01____________________

self = <test_module01.TestClass01 object at 0x762c24b0>

   def test_case01(self):
      calc = Calculator()
      result = calc.add(2, 2)
>     assert 4 == result
E     assert 4 == None

test_module01.py:9: AssertionError
=================== 1 failed in 0.32 seconds ==================

add()方法返回错误的值(即 pass ),因为此时它不做任何事情。幸运的是,pytest在测试运行中返回带有错误的那一行,这样您就可以决定需要修改什么。让我们在calculator.py中修复add()方法中的代码,如清单 8-4 所示。

class Calculator:

   def add(self, x, y):
      return x+y

Listing 8-4calculator.py

您可以再次运行测试模块。以下是输出:

===================== test session starts ====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 --
/usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter08,
inifile:
collected 1 items

test_module01.py::TestClass01::test_case01 PASSED
================== 1 passed in 0.08 seconds ===================

现在您可以向测试模块添加更多的测试用例(如清单 8-5 所示)来检查更多的特性。

from calculator import Calculator
import pytest

   class TestClass01:

      def test_case01(self):
         calc = Calculator()
         result = calc.add(2, 2)
          assert 4 == result

      def test_case02(self):
         with pytest.raises(ValueError):
             result = Calculator().add(2, 'two')

Listing 8-5test_module01.py

清单 8-5 中显示的修改后的代码试图添加一个整数和一个字符串,这会引发一个ValueError异常。

如果运行修改后的测试模块,您会得到以下结果:

===================== test session starts ====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 -- / usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter08,
inifile:
collected 2 items

test_module01.py::TestClass01::test_case01 PASSED test_module01.py::TestClass01::test_case02 FAILED

========================= FAILURES ============================
__________________ TestClass01.test_case02_____________________

self = <test_module01.TestClass01 object at 0x7636f050>

   def test_case02(self):
      with pytest.raises(ValueError):
>     result = Calculator().add(2, 'two')

test_module01.py:14:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ __ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <calculator.Calculator object at 0x7636faf0>, x = 2, y = 'two'
   def add(self, x, y):
>      return x+y
E      TypeError: unsupported operand type(s) for +: 'int' and 'str'

calculator.py:4: TypeError
============= 1 failed, 1 passed in 0.33 seconds ==============

正如您在输出中看到的,第二个测试失败了,因为它没有检测到一个ValueError异常。因此,让我们添加检查两个参数是否都是数字的规定,否则抛出一个ValueError异常—参见清单 8-6 。

class Calculator:

   def add(self, x, y):
      number_types = (int, float, complex)

         if isinstance(x, number_types) and isinstance(y, number_types):
            return x + y
        else:
           raise ValueError

Listing 8-6calculator.py

最后,清单 8-7 展示了如何向测试模块添加两个以上的测试用例,以检查add()是否如预期的那样运行。

from calculator import Calculator
import pytest

class TestClass01:

   def test_case01(self):
      calc = Calculator()
      result = calc.add(2, 2)
       assert 4 == result

   def test_case02(self):
      with pytest.raises(ValueError):
         result = Calculator().add(2, 'two')

   def test_case03(self):
      with pytest.raises(ValueError):
         result = Calculator().add('two', 2)

   def test_case04(self):
       with pytest.raises(ValueError):
         result = Calculator().add('two', 'two')

Listing 8-7test_module01.py

当您运行清单 8-7 中的测试模块时,您会得到以下输出:

===================== test session starts =====================
platform linux -- Python 3.4.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 -- / usr/bin/python3
cachedir: .cache
rootdir: /home/pi/book/code/chapter08,
inifile:
collected 4 items

test_module01.py::TestClass01::test_case01 PASSED test_module01.py::TestClass01::test_case02 PASSED test_module01.py::TestClass01::test_case03 PASSED test_module01.py::TestClass01::test_case04 PASSED

============== 4 passed in 0.14 seconds ================

这就是 TDD 在现实项目中的实现方式。您首先编写一个失败的测试,重构开发代码,继续相同的过程,直到测试通过。当您想要添加一个新特性时,您可以重复这个过程来实现它。

结论

在这一章中,你学习了容易的测试发现的编码和文件名约定;这些约定可以在所有自动化框架中实现。您还阅读了对 TDD 的简要介绍。

这本书首先介绍了 Python,包括如何在各种操作系统上安装 Python,以及 Python 版本 2 和版本 3 之间的区别。后续章节探讨了 Python 最常用的测试自动化框架。

第二章探讨了文档串并解释了它们在写作中的用处

您了解到doctest不是一个非常强大的测试框架,因为它缺少真正测试框架的许多要素。

在第三章中,向您介绍了 Python 的包含电池的测试自动化框架unittest。您学习了如何用unittest为 Python 编写xUnit风格的测试用例。

在第四章中,你探索了一个更高级的,但是已经废弃的,叫做nose的测试自动化框架。您了解了由nose提供的高级特性和插件。因为nose还没有开发出来,所以这个章节使用nose2作为运行noseunittest测试的测试程序。

在第五章中,你学习并探索了 Python 最好的单元测试自动化框架之一,pytest。你学会了如何以及为什么它比unittestnose更好。您还探索了它的插件和模块化夹具。

在第六章中,您学习并探索了 Selenium 浏览器自动化框架,它对于使用 web 浏览器的各种 web 相关编程的自动化测试用例非常有用。

第七章探索了用测井仪和loguru进行测井。日志对于开发人员和测试人员来说是一个非常有用的特性。

您已经在整本书中练习了大量的例子,其目的是向您灌输 Python 测试自动化的信心。您还学习了使用代码库,在那里他们已经用unittestdoctestnose实现了测试自动化,并计划迁移到pytest。现在,您可以编写自己的例程,并使用日志记录来记录错误。您还可以自动化与 web 相关的测试用例。此外,如果您是职业 Python 开发人员或自动化专家,您可以在项目中遵循 TDD 方法。我希望你喜欢读这本书,就像我喜欢写它一样。祝 Pythoning 和测试愉快!!

posted @ 2024-08-10 15:27  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报