原文:Python Unit Test Automation
协议:CC BY-NC-SA 4.0
四、
nose
和
nose2
上一章介绍了
xUnit
和
unittest
。在这一章中,你将探索 Python 的另一个单元测试 API,叫做
nose
。它的口号是 nose extends unittest 使测试更容易。
您可以使用
nose
的 API 来编写和运行自动化测试。你也可以使用
nose
来运行在其他框架中编写的测试,比如
unittest
。本章还探讨了下一个积极开发和维护的
nose
、
nose2
的迭代。
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 所示。
图 4-1
第四章目录结构
仅将所有代码示例保存到
test
目录。
一个简单的
nose
测试案例
清单 4-1 展示了一个非常简单的
nose
测试用例。
deftest_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 in0.007s
OK
可以通过添加如下的
-v
命令行选项来调用详细模式:
python3 -m nose -v test_module01.py
输出如下所示:
test.test_module01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in0.007s
OK
使用 nosetests 运行测试模块
您可以使用
nose
的
nosetests
命令运行测试模块,如下所示:
nosetests test_module01.py
输出如下所示:
.----------------------------------------------------------
Ran 1 test in0.006s
OK
可以按如下方式调用详细模式:
nosetests test_module01.py -v
输出如下所示:
test.test_module01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in0.007s
OK
使用
nosetests
命令是运行测试模块最简单的方法。由于编码和调用风格的简单和方便,我们将使用
nosetests
来运行测试,直到我们介绍和解释
nose2
。如果命令在 Windows 中返回一个错误,您可以用 Python 解释器调用
nose
模块。
获得帮助
使用以下命令获取关于
nose
的帮助和文档:
nosetests -h
python3 -m nose -h
组织测试代码
在前一章中,您学习了如何在不同的目录中组织项目的开发和测试代码。在这一章和下一章中,你也将遵循同样的标准。首先创建一个测试模块来测试
mypackage
中的开发代码。将清单 4-2 所示的代码保存在
test
目录中。
from mypackage.mymathlib import*classTestClass01:deftest_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
是否为
true
或
false
,以将测试方法标记为
PASS
或
FAIL
。
同样,创建一个
init.py
文件,将清单 4-3 中的代码放在
test
目录中。
all=["test_module01","test_module02"]
Listing 4-3init.py
在这之后,
chapter04
目录结构将类似于图 4-2 。
图 4-2
第四章目录结构
测试包现在准备好了。您可以从
chapter04
目录运行测试,如下所示:
nosetests test.test_module02 -v
输出如下所示:
test.test_module02.TestClass01.test_case01 ... ok
----------------------------------------------------------
Ran 1 test in0.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 in0.328s
OK
正如您在输出中看到的,
nosetests
自动发现测试包并运行它的所有测试模块。
类、模块和方法的夹具
nose
提供了
xUnit
风格的夹具,其行为方式与
unittest
中的夹具相似。甚至灯具的名称也是一样的。考虑清单 4-4 中的代码。
from mypackage.mymathlib import*
math_obj =0defsetUpModule():"""called once, before anything else in this module"""print("In setUpModule()...")global math_obj
math_obj = mymathlib()deftearDownModule():"""called once, after everything else in this module"""print("In tearDownModule()...")global math_obj del math_obj
classTestClass02:@classmethoddefsetUpClass(cls):"""called once, before any test in the class"""print("In setUpClass()...")defsetUp(self):"""called before every test method"""print("\nIn setUp()...")deftest_case01(self):print("In test_case01()")assert math_obj.add(2,5)==7deftest_case02(self):print("In test_case02()")deftearDown(self):"""called after every test method"""print("In tearDown()...")@classmethoddeftearDownClass(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 in0.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 in0.011s
OK
从现在开始,在执行测试时,示例将把
-s
选项添加到
nosetests
命令中。
功能装置
在开始学习函数的 fixtures 之前,您必须理解 Python 中函数和方法之间的区别。一个函数是一段执行操作的命名代码,一个方法是一个带有额外参数的函数,该参数是它运行的对象。函数不与类相关联。一个方法总是与一个类相关联。
查看清单 4-5 中的代码作为例子。
from nose.tools import with_setup
defsetUpModule():"""called once, before anything else in this module"""print("\nIn setUpModule()...")deftearDownModule():"""called once, after everything else in this module"""print("\nIn tearDownModule()...")defsetup_function():"""setup_function(): use it with @with_setup() decorator"""print("\nsetup_function()...")defteardown_function():"""teardown_function(): use it with @with_setup() decorator"""print("\nteardown_function()...")deftest_case01():print("In test_case01()...")deftest_case02():print("In test_case02()...")@with_setup(setup_function, teardown_function)deftest_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()
的夹具。
nose
将
test_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 in0.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"]defsetUpPackage():print("In setUpPackage()...")deftearDownPackage():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 in0.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
deftest_case01():print("In test_case01()...")assert2+2==5deftest_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
assert2+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 in0.013s
FAILED (failures=2)
由于不正确的测试输入,两个测试案例都失败了。请注意这些测试方法打印的日志之间的差异。在
test_case02()
中,你会得到更多关于失败原因的信息,因为你使用的是
nose
的
assert_equals()
方法。
测试工具
有一些方法和装饰器在你自动化测试时会非常方便。这一节将介绍其中的一些测试工具。
ok_ 和 eq_
ok_
和
eq_
分别是
assert
和
assert_equals()
的简称。当测试用例失败时,它们还带有一个错误消息的参数。清单 4-8 中的代码演示了这一点。
from nose.tools import ok_, eq_
deftest_case01():
ok_(2+2==4, msg="Test Case Failure...")deftest_case02():
eq_(2+2,4, msg="Test Case Failure...")deftest_case03():
ok_(2+2==5, msg="Test Case Failure...")deftest_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 in0.015s
FAILED (failures=2)
@raises()装饰器
当您在测试之前使用
raises
装饰器时,它必须引发与
@raises()
装饰器相关的异常列表中提到的一个异常。清单 4-9 展示了这个想法。
from nose.tools import raises
@raises(TypeError, ValueError)deftest_case01():raise TypeError("This test passes")@raises(Exception)deftest_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 notraise Exception
----------------------------------------------------------
Ran 2 tests in0.012s
FAILED (failures=1)
如您所见,
test_case02()
失败了,因为它没有在应该引发异常时引发异常。你可以巧妙地利用这一点来编写负面的测试用例。
@timed()装饰器
如果您在测试中使用一个定时装饰器,测试必须在
@timed()
装饰器中提到的时间内完成才能通过。清单 4-10 中的代码演示了这个想法。
from nose.tools import timed
import time
@timed(.1)deftest_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 in0.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 in0.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 in0.009s
OK
该插件将输出保存在名为
nosetests.html
的文件中的当前位置。
图 4-3 显示了在网络浏览器中打开的
nosetests.html
文件的快照。
图 4-3
nosetests.html 档案
在控制台中创建彩色输出
到目前为止,您已经看到了生成格式化输出文件的方法。运行
nosetest
时,您一定已经观察到控制台输出是单色的(黑色背景上的白色文本,反之亦然)。名为
rednose
的插件用于创建彩色的控制台输出。您可以使用以下命令安装该插件:
sudo pip3 install rednose
安装插件后,运行以下命令:
nosetests test_module08.py -vs --rednose
图 4-4 显示了输出的屏幕截图,尽管由于已出版书籍的灰度特性,您在这里看不到彩色的。
图 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 in0.007s
OK
nose 优于 unittest 的优势
下面总结一下
nose
相对于
unittest
的优势:
- 与
unittest
不同,nose
不需要你从父类中扩展测试用例。这导致更少的代码。 - 使用
nose
,可以编写测试函数。这在unittest
中是不可能的。 nose
比unittest
拥有更多的夹具。除了常规的unittest
夹具,nose
还有包级和功能级夹具。nose
有夹具的替代名称。- 为自动化测试用例提供了许多特性。
- 测试发现在
nose
中比在unittest
中更简单,因为nose
不需要带有discover
子命令的 Python 解释器。 nose
可以轻松识别和运行unittest
测试。
nose
的缺点
nose
唯一也是最大的缺点是,它没有处于积极的开发中,过去几年一直处于维护模式。如果没有新的人或团队来接管维护工作,它很可能会停止。如果你计划开始一个项目,并且正在为 Python 3 寻找一个合适的自动化框架,你应该使用
pytest
、
nose2
或者普通的
unittest
。
你可能会奇怪,如果它没有被积极地开发,我为什么还要花时间去讨论
nose
。原因是学习像
nose
这样更高级的框架有助于你理解
unittest
的局限性。此外,如果您正在使用一个使用
nose
作为测试自动化和/或单元测试框架的老项目,它将帮助您理解您的测试。
使用 nose2
nose2
是 Python 的下一代测试。它基于
unittest2
的插件分支。
nose2
旨在从以下方面对
nose
进行改进:
- 它提供了一个更好的插件 API。
- 用户更容易配置。
- 它简化了内部接口和流程。
- 它支持来自相同代码库的 Python 2 和 3。
- 它鼓励社区更多地参与其发展。
- 与
nose
不同,它正在积极开发中。
nose2
可以使用以下命令方便地安装:
sudo pip3 install nose2
安装后,可以通过在命令提示符下运行
nose2
来调用
nose2
。
它可用于自动发现和运行
unittest
和
nose
测试模块。在命令提示符下运行
nose2 -h
命令,获得各种
nose2
命令行选项的帮助。
以下是
nose
和
nose2
的重要区别:
- 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
。
图 4-5
nose2 不支持的测试布局
您可以使用
nose2
运行测试,如下所示:
nose2 -v
您还可以参数化测试,如清单 4-12 所示。
from nose2.tools import params
@params("Test1234","1234Test","Dino Candy")deftest_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 in0.002s
FAILED (failures=2)
您可以通过修改代码直接从任何 IDE 启动测试脚本,而无需指定
nose2
模块,如清单 4-13 所示。
from nose2.tools import params
@params("Test1234","1234Test","Dino Candy")deftest_starts_with(value):assert value.startswith('Test')if __name__ =='__main__':import nose2
nose2.main()
Listing 4-13test_module20.py
您可以直接从任何 IDE(如 IDLE)启动它,它会产生相同的结果。
Exercise 4-1
检查您组织中的代码库是否在使用
unittest
、
nose
或
nose2
。咨询代码库的所有者,计划从这些框架到更好、更灵活的单元测试框架的迁移。
结论
在本章中,你学习了高级单元测试框架
nose
。不幸的是,它没有被积极开发,所以你需要使用
nose2
作为
nose
测试的测试员。在下一章中,您将了解并探索一个叫做
py.test
的高级测试自动化框架。
五、
pytest
在第四章中,您探索了
nose
,这是一个用于 Python 测试的高级且更好的框架。不幸的是,
nose
在过去的几年里没有得到积极的开发。当你想为一个长期项目选择一些东西时,这使得它不适合作为测试框架的候选。此外,有许多项目使用
unittest
或
nose
或两者的组合。你肯定需要一个比
unittest
有更多功能的框架,而且不像
nose
,它应该在积极开发中。
nose2
更像是
unittest
的测试版,几乎是一个废弃的工具。你需要一个单元测试框架,能够发现和运行用
unittest
和
nose
编写的测试。它应该是先进的,并且必须得到积极的开发、维护和支持。答案是
pytest
。
本章广泛地探索了一个现代的、先进的、更好的测试自动化框架,称为
pytest
。首先,你将了解
pytest
如何提供传统的
xUnit
风格的夹具,然后你将探索
pytest
提供的先进夹具。
pytest 简介
pytest
不是 Python 标准库的一部分。你必须安装它才能使用它,就像你安装了
nose
和
nose2
一样。让我们看看如何为 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 中的代码作为例子。
deftest_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 in0.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 in0.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 in0.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 in0.04 seconds =================
为了简单和方便起见,从现在开始,在本章和本书的剩余部分,您将使用相同的方法来运行这些测试。你将在最后一章中使用
pytest
来实现一个具有测试驱动开发方法的项目。此外,当您运行您自己的测试时,请注意测试执行的输出默认是彩色的,尽管这本书显示的结果是黑白的。你不必使用任何外部或第三方插件来实现这一效果。图 5-1 显示了一个执行样本的截图。
图 5-1
pytest 执行示例
pytest 中的测试类和测试包
像所有以前的测试自动化框架一样,在
pytest
中,您可以创建测试类和测试包。以清单 5-2 中的代码为例。
classTestClass01:deftest_case01(self):assert'python'.upper()=='PYTHON'deftest_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
可以发现并自动运行测试,就像
unittest
、
nose
和
nose2
一样。在
project
目录中运行以下命令来启动自动化测试发现:
py.test
对于详细模式,运行以下命令:
py.test -v
xUnit 风格的灯具
pytest
有
xUnit
样式的夹具。请参见清单 5-4 中的代码作为示例。
defsetup_module(module):print("\nIn setup_module()...")defteardown_module(module):print("\nIn teardown_module()...")defsetup_function(function):print("\nIn setup_function()...")defteardown_function(function):print("\nIn teardown_function()...")deftest_case01():print("\nIn test_case01()...")deftest_case02():print("\nIn test_case02()...")classTestClass02:@classmethoddefsetup_class(cls):print("\nIn setup_class()...")@classmethoddefteardown_class(cls):print("\nIn teardown_class()...")defsetup_method(self, method):print("\nIn setup_method()...")defteardown_method(self, method):print("\nIn teardown_method()...")deftest_case03(self):print("\nIn test_case03()...")deftest_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
支持
unittest
和
nose
中编写的所有测试。
pytest
可以自动发现并运行
unittest
和
nose
中编写的测试。它支持所有用于
unittest
测试类的
xUnit
风格的夹具。它还支持
nose
中的大部分夹具。尝试运行
chapter03
和
chapter04
目录中的
py.test -v
。
pytest 夹具介绍
除了支持
xUnit
风格的夹具和
unittest
夹具,
pytest
有自己的一套灵活、可扩展和模块化的夹具。这是
pytest
的核心优势之一,也是为什么它是自动化测试人员的热门选择。
在
pytest
中,您可以创建一个夹具,并在需要的地方将其作为资源使用。
以清单 5-5 中的代码为例。
import pytest
@pytest.fixture()deffixture01():print("\nIn fixture01()...")deftest_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 in0.04 seconds ====================
如您所见,
fixture01()
在测试函数
test_case01()
之前被调用。你也可以使用
@pytest.mark.usefixtures()
装饰器,它可以达到同样的效果。清单 5-6 中的代码是用这个装饰器实现的,它产生与清单 5-5 相同的输出。
import pytest
@pytest.fixture()deffixture01():print("\nIn fixture01()...")@pytest.mark.usefixtures('fixture01')deftest_case01(fixture01):print("\nIn test_case01()...")
Listing 5-6test_module05.py
清单 5-6 的输出与清单 5-5 中的代码完全相同。
你可以为一个类使用
@pytest.mark.usefixtures()
装饰器,如清单 5-7 所示。
import pytest
@pytest.fixture()deffixture01():print("\nIn fixture01()...")@pytest.mark.usefixtures('fixture01')classTestClass03:deftest_case01(self):print("I'm the test_case01")deftest_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 in0.08 seconds ====================
如果您想在使用 fixture 的测试运行之后运行一段代码,您必须向 fixture 添加一个 finalizer 函数。清单 5-8 展示了这个想法。
import pytest
@pytest.fixture()deffixture01(request):print("\nIn fixture...")deffin():print("\nFinalized...")
request.addfinalizer(fin)@pytest.mark.usefixtures('fixture01')deftest_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 in0.05 seconds =====================
pytest
提供对所请求对象的夹具信息的访问。清单 5-9 展示了这个概念。
import pytest
@pytest.fixture()deffixture01(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')deftest_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 in0.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
deftest_case01():with pytest.raises(Exception):
x =1/0deftest_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__________________________
deftest_case02():with pytest.raises(Exception):> x =1/1
E Failed: DID NOT RAISE <class'Exception'>
test_module09.py:10: Failed
==============1 failed,1 passed in0.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
测试。 - 它有自己的一套先进的装置,本质上是模块化的。
如果您正在从事一个使用
unittest
、
nose
或
doctest
作为 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 所示。
图 6-1。
铬的 Selenium IDE
您可以从以下 URL 访问 Firefox 浏览器的附加组件:
https://addons.mozilla.org/en-GB/firefox/addon/selenium-ide/
添加后,可以从地址栏旁边的菜单中访问,如图 6-2 右上角所示。
图 6-2。
铬的 Selenium IDE
在各自的浏览器中点击这些选项会打开一个窗口,如图 6-3 所示。
图 6-3。
Selenium IDE 窗口
Selenium IDE 的 GUI 对于所有浏览器都是一样的。点击新建项目,打开新窗口,如图 6-4 所示。
图 6-4。
Selenium 新项目
输入您选择的名称。这将启用确定按钮。点击确定按钮,显示图 6-5 中的窗口。
图 6-5。
Selenium IDE 窗口
如您所见,该窗口分为多个部分。在左上方,您可以看到项目的名称。在右上角,有三个图标。单击第一个图标会创建一个新项目。第二个图标用于打开现有项目。第三个图标保存当前项目。保存的文件有一个
*.side
扩展名(Selenium IDE)。
让我们重命名现有的测试。检查左侧选项卡。可以看到一个未命名的测试,如图 6-6 所示。
图 6-6。
重命名未命名的测试
当您保存项目时,它会尝试将其保存为一个新文件。您必须通过覆盖先前的文件来用现有的名称保存它。现在,单击录制按钮。快捷键是 Ctrl+U,它会打开一个新的对话框,要求您输入项目的基本 URL 见图 6-7 。
图 6-7
项目基本 URL
你必须输入要测试的网页的网址。URL 还应该包含文本
http://
或
https://
,否则不会将其视为 URL。在 http://www.google.com 输入框中输入。然后,它将启用开始录制按钮。录制按钮是红色的,位于窗口的右上角。点击按钮,它将启动一个新的窗口与指定的网址。看起来像图 6-8 。
图 6-8。
Selenium IDE 记录
在搜索栏中输入 Python ,然后点击谷歌搜索。它会向您显示搜索结果。单击第一个结果,然后在加载页面后,关闭浏览器窗口。然后单击菜单中的按钮停止录制。你会在 IDE 中看到记录的步骤,如图 6-9 所示。
图 6-9。
记录后的 Selenium IDE
您可以自动重新运行所有步骤。您可以在录制按钮的同一栏中看到一组四个图标。第一个图标用于运行所有测试,第二个图标用于运行当前测试。当前项目只有一个测试,因此它将运行套件中唯一的测试。单击任一按钮,自动重复这一系列操作。
这样你就可以记录和执行一系列的动作。一旦记录的测试成功执行,底部将显示日志,如图 6-10 所示。
图 6-10。
Selenium IDE 日志
您可以通过单击菜单中的+图标向项目中添加新的测试。一个项目通常会有多个测试。现在,您将学习如何导出项目。你可以右击测试打开菜单,如图 6-6 所示。单击导出选项。它打开一个新窗口,如图 6-11 所示。
图 6-11。
将项目导出为代码
选中顶部的两个选项,然后单击导出按钮。它将打开一个名为另存为的窗口。提供详细信息,它会将项目保存为 Python 文件,扩展名为
*.py
,保存在指定的目录中。生成的代码如清单 6-1 所示。
# Generated by Selenium IDEimport 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
classTestTest01():defsetup_method(self, method):
self.driver = webdriver.Chrome()
self.vars={}defteardown_method(self, method):
self.driver.quit()deftest_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
classTestClass01(unittest.TestCase):defsetUp(self):
driver_path=r'D:\\drivers\\geckodriver.exe'
driver = webdriver.Firefox(executable_path=driver_path)
self.driver = driver
print("\nIn setUp()...")deftearDown(self):print("\nIn tearDown")
self.driver.close()
self.driver.quit()deftest_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-0119:09:14.900123
Iteration 1...
Iteration 2...
Iteration 3...
Iteration 4...
Iteration 5...
Done...2021-09-0119: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
开始的所有记录级别都将被记录。其中包括
Warning
、
Error
、
Critical
。
您可以更改日志记录的级别,如清单 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()
例程配置了日志记录的级别。在调用任何日志例程之前,您需要调用这个例程。该示例将日志记录级别设置为
Debug
。
Debug
是最低级别的日志记录,这意味着所有日志记录级别为
Debug
及以上的日志都将被记录。输出如下所示:
DEBUG:root:Debug
INFO:root:Info
WARNING:root:Warning
ERROR:root:Error
CRITICAL:root:Critical
让我们详细看看日志消息。如您所见,日志消息分为三部分。第一部分是日志的级别。第二部分是记录器的名称。在这种情况下,它是根日志记录器。第三部分是传递给日志例程的字符串。稍后您将了解如何更改此消息的详细信息。
这是讨论不同日志记录级别的含义的好时机。
Debug
和
Info
级别通常表示程序的一般执行。
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-0213:36:35,401:DEBUG:Debug
2021-09-0213:36:35,401:INFO:Info
2021-09-0213:36:35,401:WARNING:Warning
2021-09-0213:36:35,401:ERROR:Error
2021-09-0213: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-0300:25:40,135:Logger1:DEBUG:Debug
2021-09-0300:25:40,153:Logger2:DEBUG:Debug
2021-09-0300:25:40,161:Logger1:INFO:Info
2021-09-0300:25:40,168:Logger2:INFO:Info
2021-09-0300:25:40,176:Logger1:WARNING:Warning
2021-09-0300:25:40,184:Logger2:WARNING:Warning
2021-09-0300:25:40,193:Logger1:ERROR:Error
2021-09-0300:25:40,200:Logger2:ERROR:Error
2021-09-0300:25:40,224:Logger1:CRITICAL:Critical
2021-09-0300: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-0315:13:37,513:Logger1:DEBUG:Debug
2021-09-0315:13:37,533:Logger1:INFO:Info
2021-09-0315:13:37,542:Logger1:WARNING:Warning
2021-09-0315:13:37,552:Logger1:ERROR:Error
2021-09-0315:13:37,560:Logger1:CRITICAL:Critical
日志文件的输出如下:
2021-09-0315:13:37,532- Logger2 - DEBUG - Debug
2021-09-0315:13:37,542- Logger2 - INFO - Info
2021-09-0315:13:37,551- Logger2 - WARNING - Warning
2021-09-0315:13:37,560- Logger2 - ERROR - Error
2021-09-0315:13:37,569- Logger2 - CRITICAL – Critical
用线程记录日志
有时,你会在你的程序中使用多线程。Python 允许对线程使用日志记录功能。这可以确保您了解程序中使用的线程的执行细节。创建一个新的 Python 文件,将其命名为
prog11.py
。将以下代码添加到该文件中:
import logging
import threading
import time
现在创建一个函数,如下所示:
defworker(arg, number):whilenot 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()
创建一个将被键盘中断的循环,同时也会中断线程:
whileTrue:try:
logging.debug('Hello from the main() thread')
time.sleep(1)except KeyboardInterrupt:
info['stop']=Truebreak
最后,连接这些线程:
thread1.join()
thread2.join()
整个程序如清单 7-12 所示。
import logging
import threading
import time
defworker(arg, number):whilenot 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()whileTrue:try:
logging.debug('Hello from the main() thread')
time.sleep(1)except KeyboardInterrupt:
info['stop']=Truebreak
thread1.join()
thread2.join()
Listing 7-12prog11.py
运行程序,几秒钟后按 Ctrl+C 终止程序。输出如下所示:
2021-09-0315:34:27,071:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:27,304:root:DEBUG:Hello from the main() thread
2021-09-0315:34:27,664:root:DEBUG:Hello from worker() thread number 22021-09-0315:34:27,851:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:28,364:root:DEBUG:Hello from the main() thread
2021-09-0315:34:28,629:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:29,239:root:DEBUG:Hello from worker() thread number 22021-09-0315:34:29,381:root:DEBUG:Hello from the main() thread
2021-09-0315:34:29,414:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:30,205:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:30,444:root:DEBUG:Hello from the main() thread
2021-09-0315:34:30,788:root:DEBUG:Hello from worker() thread number 22021-09-0315:34:30,990:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:31,503:root:DEBUG:Hello from the main() thread
2021-09-0315:34:31,828:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:32,311:root:DEBUG:Hello from worker() thread number 22021-09-0315:34:32,574:root:DEBUG:Hello from the main() thread
2021-09-0315:34:32,606:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:33,400:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:33,634:root:DEBUG:Hello from the main() thread
2021-09-0315:34:33,865:root:DEBUG:Hello from worker() thread number 22021-09-0315:34:34,175:root:DEBUG:Hello from worker() thread number 12021-09-0315:34:34,688:root:DEBUG:Hello from the main() thread
2021-09-0315:34:34,969:root:DEBUG:Hello from worker() thread number 12021-09-0315: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-0316:10:53,938:Logger1:DEBUG:Debug
2021-09-0316:10:53,956:Logger2:DEBUG:Debug
2021-09-0316:10:53,966:Logger1:INFO:Info
2021-09-0316:10:53,974:Logger2:INFO:Info
2021-09-0316:10:53,983:Logger1:WARNING:Warning
2021-09-0316:10:53,993:Logger2:WARNING:Warning
2021-09-0316:10:54,002:Logger1:ERROR:Error
2021-09-0316:10:54,011:Logger2:ERROR:Error
2021-09-0316:10:54,031:Logger1:CRITICAL:Critical
2021-09-0316:10:54,049:Logger2:CRITICAL:Critical
日志文件包含程序执行后的以下日志:
2021-09-0316:10:53,938- Logger1 - DEBUG - Debug
2021-09-0316:10:53,956- Logger2 - DEBUG - Debug
2021-09-0316:10:53,966- Logger1 - INFO - Info
2021-09-0316:10:53,974- Logger2 - INFO - Info
2021-09-0316:10:53,983- Logger1 - WARNING - Warning
2021-09-0316:10:53,993- Logger2 - WARNING - Warning
2021-09-0316:10:54,002- Logger1 - ERROR - Error
2021-09-0316:10:54,011- Logger2 - ERROR - Error
2021-09-0316:10:54,031- Logger1 - CRITICAL - Critical
2021-09-0316: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.4in 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)deffunction1(a, b):return a / b
deffunction2(c):try:
function1(5, c)except ZeroDivisionError:
logger.exception('Divide by Zero!')
function2(0)
Listing 7-17Prog16.py
这些附加参数允许您详细跟踪故障。日志文件有以下输出:
2021-09-0317: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 所示的输出。
图 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(测试驱动开发)。
版权归原作者 绝不原创的飞龙 所有, 如有侵权,请联系我们删除。