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

133 阅读1小时+

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

原文:Python Unit Test Automation

协议:CC BY-NC-SA 4.0

四、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(测试驱动开发)。