part16:Python文档和测试(pydoc模块使用,文档测试doctest.testmod(),单元测试PyUnit:白盒测试和回归测试,单元测试模块unittest,跳过测试用例)


知识点:

  • 使用 pydoc 在控制台中查看文档
  • 使用 pydoc 生成 HTML 页面
  • 使用 pydoc 启动本地服务器来查看帮助文档
  • 使用 pydoc 查找模块
  • 软件测试的概念、目的和分类
  • Python 提供的文档测试工具的用法
  • 单元测试基本概念
  • 单元测试逻辑覆盖
  • unittest 的功能和用法
  • 使用测试包来组织测试用例
  • 使用 setUp 和 tearDown 来管理测试固件
  • 跳过测试用例的方法

软件测试:检验软件产品是否满足实际需要。

理论上,软件测试由测试人员完成。软件测试的目的不是修复软件,也不是证明软件是满足要求的、可用的,而只是找出软件中存在的缺陷,然后将这些缺陷提交给 Bug 管理系统,修复工作与测试人员无关,应该由软件开发人员来完成。

传统软件开发流程(如瀑布模型)中,习惯将软件测试放在软件开发之后(即软件开发完成后才进行软件测试)。

实际上,目前的软件开发流程是,软件开发和软件测试是同步进行的。开发人员不断为系统开发新功能,而测试人员不断的测试它们,并找出这些新功能中可能存在的缺陷,并提交给 Bug 管理系统,开发者在修复这些缺陷。更有甚者,要求先进行测试,也就是先提供测试用例,再开发满足测试要求的软件(如测试驱动开发)。

总之一句话,软件测试也成为软件工程中重要的一环,不可分割。


一、使用 pydoc 生成文档

前面使用的help() 函数__doc__属性来查看函数、类、方法的文档,总是在控制器中查看,难免不太方便。使用 Python 自带的 pydoc 模块,可以方便的查看、生成帮助文档,该文档是 HTML 格式,查看、使用起来非常方便。

为了使用 pydoc 模块,首先创建一个Python 文件,文件名是 mymodule.py,代码如下:

MY_NAME = 'Michael'

def say_hi(name):
    """
    定义一个打招呼的函数
    返回对指定用户打招呼的字符串
    :param name: 打招呼的用户
    :return: 打招呼的字符串
    """

def print_rect(height, width):
    """
    打印矩形的函数
    :param height: 矩形的高
    :param width: 矩形的宽
    :return: 无返回值
    """
    print(("*" * width + "\n") * height)

class User:
    NATIONAL = 'China'
    """
    一个代表用户的类
    该类包括name、age 两个变量
    """
    def __init__(self, name, age):
        """
        :param name: 初始化该用户的 name
        :param age:  初始化该用户的 age
        """
        self.name = name
        self.age = age

    def eat(self, food):
        """
        用户吃东西的方法
        :param food: 用户正在吃的东西
        :return: 无返回值
        """
        print('%s正在吃%s' % (self.name, food))

上面定义的 mymodule.py 文件,也是一个模块,在模块中为函数、类和方法都提供了文档说明。


1、在控制台中查看文档

使用 pydoc 模块可在控制台查看 HTML 帮助文档。在控制台中的语法如下:

python -m pydoc 模块名
  • m参数:python 命令的一个选项,表示运行指定模块,这里运行的是 pydoc模块;
  • 模块名参数:是要查看的模块。

例如在命令中查看前面的 mymodule.py 文件,示例如下:

python -m pydoc mymodule

在命令行中看到的输出结果与使用 help() 命令查看帮助信息的差别相差不大。但是 pydoc 输出的帮助信息,有自己的组织方式,组织方式的顺序是:

  • 模块的文档说明:就是 *.py 文件顶部的注释信息,会被提取成模块的文档说明。
  • CLASSES 部分:列出该模块所包含的全部类帮助信息;
  • FUNCTIONS部分:列出该模块所包含的全部函数帮助信息;
  • DATA 部分:列出该模块所包含的全部成员变量;
  • FILE 部分:显示该模块对应的源文件。

2、生成 HTML 文档

使用 pydoc 模块在控制台中查看帮助文档的命令如下:

python -m pydoc -w 模块名
  • 参数w:代表 write,表明输出 HTML 文档

例如要给 mymodule.py 文件生成 HTML 文档,可执行下面命令:

python -m pydoc -w mymodule

运行上面这个命令会看到提示信息“wrote mymodule.html”,此时在该目录下会多出一个 mymodule.html 文件,使用浏览器打开这个文件,同样可以看到在帮助信息。该HTML 的帮助信息基本与控制台的一样,区别是在 HTML 页面可上、下滚动屏幕,方便反复查看。

pydoc 还可为指定目录下的所有模块生成 HTML 文档,使用方法如下:

python -m pydoc -w 目录名

上面的命令有一个缺陷是:当该命令工具要展示目录下子文件的说明时, 会去子目录下找对应的 .html 文件,如果文件不存在,就会显示404 错误。

如果确实要查看指定目录下所有子目录中的文档信息, 可以启动本地服务器来查看。


3、启动本地服务器来查看文档信息

有两个命令可以使用:

# 命令1
python -m pydoc -p 端口号
# 命令2
python -m pydoc -b
  • 命令1 可以指定端口来启动 HTTP 服务器;
  • 命令2 会使用一个未占用的端口来启动 HTTP 服务器;
  • 启动HTTP服务器后,就可通过浏览器查看 Python 的所有模块的文档信息。

例如在命令运行下面命令:

python -m pydoc -p 8801

此时会显示如下的提示信息:

Server ready at http://localhost:8801/
Server commands: [b]rowser, [q]uit
server>

从提示信息可知,HTTP 服务器正在 8801 端口提供服务,此时可输入 b 命令启动浏览器,输入 q 命令停止服务器。

在浏览器中访问 http://localhost:8801/,就可以看到当前 Python 的所有模块的帮助信息。其中:

第1部分:显示 Python 内置的核心模块。
第2部分:显示当前目录下的所有模块,这里显示的就是 mymodule 模块。
第3部分:显示 PYTHONPATH 环境变量所指定路径下的模块,自定义的模块也在这部分显示。
往下还有 lib 目录下的模块、site-packages 目录下的模块等等。


4、查找模块

pydoc 提供的 -k 选项可用于查找模块,语法规范如下:

python -m pydoc -k 被搜索模块的部分内容

例如运行下面命令,查找 mymodule 模块:

python -m pydoc -k mym	# 得到的输出信息如下
mymodule

输出信息是 pydoc 找到了包含 mym 的 mymodule。


二、软件测试

软件测试是保证软件质量的重要手段。软件开发和软件测试通常由不同的人来完成。某些情况下(缺少软件测试人员)软件开发人员也会直接参与软件测试。

软件测试通常占软件开发工作量的 40%,在某些关系到生命、财产安全的软件测试成本,会比软件开发的成本还要高。


1、软件测试概念、目的

软件测试包括在软件产品生存周期内所有的检查、评审和确认活动,如设计评审、系统测试等。软件测试也可单指对软件产品的质量进行测试。

IEEE 对软件测试的定义:

测试是使用人工和自动手段来运行或检测某个系统的过程,其目的在于检验系统是否满足规定的需求,或者弄清预期结果与实际结果之间的差别。

Gl en Myers (梅尔斯)定义的软件测试(更多人接受):

  • 软件测试是为了发现软件隐藏的缺陷。
  • 一次成功的软件测试是发现了尚未被发现的缺陷。
  • 软件测试并不能保证软件没有缺陷。

用户角度:希望通过软件测试来发现软件中潜在的缺陷和问题, 以考虑是否需要接受该软件产品;

开发人员角度:希望测试成为软件产品中不存在缺陷和问题的证明过程,从而表明该软件产品已经能满足用户的需求。

软件测试并不能保证软件质量。软件质量保证贯穿整个软件开发过程,从需求分析开始,到最后的系统上线。

软件测试的目的:以最少的时间和人力,系统地找出软件中潜在的各种错误和缺陷。

由于软件的复杂性,引起软件缺陷的来源也很多,常见的有:

  • 编程错误: 只要是程序员,就有可能犯错误。
  • 软件的复杂度: 软件的复杂度随软件的规模以指数级数增长,软件的分布式应用、数据通信、多线程处理等都增加了软件的复杂度。
  • 不断变更的需求: 软件的需求定义总是滞后于实际的需求,如果实际的需求变更太快,软件就难以成功。
  • 时间的压力:为了追上需求的变更,软件的时间安排非常紧张,随着最后期限的到来,缺陷被大量引入。
  • 开发平台本身的缺陷:类库、编译器、链接器本身也是程序,它们也可能存在缺陷,新开发的系统也就无法幸免。

对软件测试了上百万次,也不能保证软件没有缺陷。软件测试是不可穷举的,软件测试是在成本与效果之间的平衡选择。

软件测试在软件生命周期中横跨两个阶段: 通常在编写出每个模块之后,程序开发者应该完成必要的测试,这种测试称为单元测试;在各个阶段结束后,还必须对软件进行各种综合测试,这部分的测试通常由测试人员来完成。

测试的目的是发现程序的缺陷,不是为了证明程序是正确的。

软件测试不应该由开发人员完成,而是由测试人员完成。


三、文档测试

可通过 doctest 模块运行 Python 源文件中的说明文档中的测试用例来生成测试报告。例如在查看 Python 模块的源文件时,经常看到下面的信息。

>>> os.path.commonprefix(['/usr/lib', '/usr/local/lib'])
'/usr/l'
>>> os.path.commonpath(['/usr/lib', '/usr/local/lib'])
'/usr'

上面的这些信息就是文档测试的注释,文档测试工具可以提取说明文档中的 “>>>” 后测试用例,随后的就是测试用例的输出结果。文档测试工具会判断测试用例的运行结果与输出结果是否一致,如果不一致就会显示错误信息。

现在定义一个简单的模块,模块中有一个函数和一个类,并且为函数和类提供了说明文档,说明文档中包含有测试用例,代码如下:

def square(x):
    '''
    一个用于计算平方的函数
    example:
    >>> square(2)
    4
    >>> square(3)
    9
    >>> square(-3)
    9
    >>> square(0)
    0
    '''
    return x * 2    # 故意写错进行测试

class User:
    '''
    定义一个代表用户的类,该类包含下面两个属性
    name - 代表用户的名字
    age - 代表用户的年龄
    example:
    >>> u = User('Tom', 9)
    >>> u.name
    'Tom'
    >>> u.age
    9
    >>> u.say('hello world')
    'Tom 说:hello world'
    '''
    def __init__(self, name, age):
        self.name = 'tom'   # 故意写错进行测试
        self.age = age
    def say(self, content):
        return self.name + ' 说:' + content

if __name__ == '__main__':
    import doctest
    doctest.testmod()

上面代码中为 square() 函数提供了4测试用例;为 User 类提供了 3 个测试用例。最的一行代码调用 doctest 模块的 testmod() 函数进行测试。测试结果如下:

**********************************************************************
File "test_module.py", line 23, in __main__.User
Failed example:
    u.name
Expected:
    'Tom'
Got:
    'tom'
**********************************************************************
File "test_module.py", line 27, in __main__.User
Failed example:
    u.say('hello world')
Expected:
    'Tom 说:hello world'
Got:
    'tom 说:hello world'
**********************************************************************
File "test_module.py", line 7, in __main__.square
Failed example:
    square(3)
Expected:
    9
Got:
    6
**********************************************************************
File "test_module.py", line 9, in __main__.square
Failed example:
    square(-3)
Expected:
    9
Got:
    -6
**********************************************************************
2 items had failures:
   2 of   4 in __main__.User
   2 of   4 in __main__.square
***Test Failed*** 4 failures.

从输出可知,有4个测试没有通过,其中 User 类两个测试没有通过,square() 函数有两个测试没有通过。在每个测试输出结果中,都包含了4个部分:

第1部分:包含源文件名、模块名、以及对应哪一行;
第2部分:Failed example 是批哪个测试用例出错;
第3部分:Expected 是显示程序期望的输出结果,也就是在 ">>>" 命令的下一行给出的运行结果;
第4部分:Got,是实际的测试程序输出结果。只有当输出结果与期望结果一致,才表示测试通过。

Python的 doctest 模块的 testmod() 函数会自动提取模块中的说明文档中的测试用例,并执行这些测试用例,最终生成测试报告。没有通过的测试用例就会输出,通过的测试用例不会有任何输出。


四、使用 PyUnit(unittest),单元测试构架

PyUnit 是Python 自带的单元测试框架,用于编写和运行可重复的测试。PyUnit 是 xUnit 体系的一个成员, xUnit 是众多测试框架的总称, PyUnit 主要用于进行白盒测试和回归测试。

通过 PyUnit 可以让测试具有持久性,测试与开发同步进行,测试代码与开发代码一同发布。PyUnit 的好处:

  • 可以使测试代码与产品代码分离。
  • 针对某一个类的测试代码只需要进行较少的改动,便可以应用于另一个类的测试。
  • PyUnit 开放源代码,可以进行二次开发,方便对 PyUnit 的扩展。

PyUnit 一个简单、易用的测试框架,有如下特征:

  • 使用 断言方法 判断期望值和实际值的差异,返回 bool 值。
  • 测试驱动设备可使用共同的初始化变量或实例。
  • 测试包结构便于组织和集成运行。

1、使用 PyUnit(unittest)

测试的本质:通过给定参数来执行函数,然后判断函数的实际输出结果和期望输出结果是否一致。

测试驱动开发:先编写测试用例,然后再编写函数和该当。假如要开发一个满足 A 功能的 fun_a() 函数,采用测试驱动开发的步骤如下:

  1. 为 fun_a() 函数编写测试用例,根据业务要求,使用大量不同的参数组合来执行 fun_a() 函数,并断言该函数的执行结果与业务期望的执行结果匹配。
  2. 编写、修改 fun_a() 函数。
  3. 运行 fun_a() 函数的测试用例,如果测试用例不能完全通过;则重复第2 步和第3 步,直到 fun_a() 的所有测试用例全部通过。

下面这段代码所在的文件是 my_math.py,该文件中有两个函数,分别用于计算一元一次方程的解和二元一次方程的解。代码如下:

def one_equation(a, b):
    """
    求一元一次方程的 a * x + b = 0 的解
    :param a: 方程中变量的系数
    :param b: 方程中的常量
    :return: 方程的解
    """
    # 如果 a = 0 则方程无法求解
    if a == 0:
        raise ValueError("参数错误")
    # 返回方程的解
    else:
        return -b / a

def two_equation(a, b, c):
    """
    求一元二次方程 a * x * x + b * x + c = 0 的解
    :param a: 方程中变量二次幂的系数
    :param b: 方程中变量的系数
    :param c: 方程中的常量
    :return: 方程的解
    """
    # 如果 a == 0, 则变成一元一次方程
    if a == 0:
        raise ValueError("参数错误")
    # 在有理数范围内无解
    elif (b * b - 4 * a * c) < 0:
        raise ValueError("方程在有理数范围内无解")
    # 方程有唯一的解
    elif (b * b - 4 * a * c) == 0:
        return -b / (2 * a)
    # 方程有两个解
    else:
        r1 = (-b + (b * b - 4 * a * c) ** 0.5) / 2 / a
        r2 = (-b - (b * b - 4 * a * c) ** 0.5) / 2 / a
        return r1, r2

上面的 my_math.py 文件就相当于是一个模块,下面就为该模块编写单元测试代码。

unittest 要求单元测试类必须继承 unittest.TestCase,该类中的测试方法需要满足下面的要求:

  • 测试方法应该没有返回值。
  • 测试方法不应该有任何参数。
  • 测试方法应以 test_ 开头。

测试用例所需的代码如下:

import unittest
from my_math import *

class TestMyModule(unittest.TestCase):
    # 测试一元一次方程的求解
    def test_one_equation(self):
        # 断言该方程的解应该为 -1.8
        self.assertEqual(one_equation(5, 9), -1.8)
        # 断言该方程的解应该为 -2.5
        self.assertTrue(one_equation(4, 10) == -2.5, .00001)
        # 断言该方程的解应该为 27/4
        self.assertTrue(one_equation(4, -27) == 27/4)
        # 断言当 a == 0 时的情况,断言引发 ValueError
        with self.assertRaises(ValueError):
            one_equation(0, 9)

    # 测试一元二次方程的求解
    def test_two_equation(self):
        r1, r2 = two_equation(1, -3, 2)
        self.assertCountEqual((r1, r2), (1.0, 2.0), '求解出错')
        r1, r2 = two_equation(2, -7, 6)
        self.assertCountEqual((r1, r2), (1.5, 2.0), '求解出错')
        # 断言只有一个解的情况
        r = two_equation(1, -4, 4)
        self.assertEqual(r, 2.0, '求解出错')
        # 断言当 a == 0 时的情况,断言引发 ValueError
        with self.assertRaises(ValueError):
            two_equation(0, 9, 3)
        # 断言引发 ValueError
        with self.assertRaises(ValueError):
            two_equation(4, 2, 3)

上面代码中使用了断言方法判断函数的实际输出结果与期望输出结果是否一致,如果一致表明测试通过,否则表明测试失败。

上面的测试用例中,对 one_equation() 方法的测试进行了4次。具体有测试次数取决于测试人员要求达到怎样的逻辑覆盖程序,测试要求越高,则需要更多次数的测试。这里仅介绍 PyUnit 的用法,具体测试的逻辑覆盖,要根据需求来定。

unittest.TestCase 模块内置了大量 assert.Xxx 方法来执行断言,常用的断言方法如下表所示:

断言方法 检查条件
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isintance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

对于异常、错误、警告和日志进行的断言判断,TestCase 有下面这些断言方法:

断言方法 检查条件
assertRaise(exc, fun, *args, **kwds) fun(*args, **kwds)引发 exc 异常
assertRaiseRegex(exc, r, fun, *arg, **kwds) fun(*args, **kwds)引发 exc 异常,且异常信息匹配 r 正则表达式
assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds)引发 warn 警告
assertWarnsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds)引发 warn 警告,且警告信息匹配 r 正则表达式
assertLogs(logger, level) With 语句快使用日志器生成 level 级别的日志

TestCase 还有用于完成某种特定检查的断言方法:

断言方法 检查条件
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)
assertNotRegex(s, r) not r.search(s)
assertCountEqual(a, b) a, b两个序列包含的元素相同,不管元素出现的顺序如何

在使用 assertEqual() 判断两个对象是否相等时,如果被判断的类型是字符串、序列、列表、元组、集合、字典,unittest 会自动改为使用下表的断言方法进行判断。也就是说,下表的断言没有必要使用,unittest 模块会自动使用它们。这些方法如下:

断言方法 用于比较的类型
assertMultil.ineEqual(a, b) 字符串(string)
assertSequenceEqual(a, b) 序列(sequence)
assertListEqual(a, b) 列表(list)
assertTupleEqual(a, b) 元组(tuple)
assertSetEqual(a, b) 集合(set 或 frozenset)
assertDictEqual(a, b) 字典(dict)

2、运行测试

有了测试用例后,有两种方式来运行。

方式1:在测试用例源代码文件中调用测试用例。即调用 unittest.main() 运行当前源文件的所有测试用例,在测试用例源文件最后面添加下面代码:

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

有了上面这段代码,现在直接运行 test_my_math.py 文件,就会运行当前源文件中的所有测试用例。

方式2:使用 unittest 模块运行测试用例,语法如下:

python -m unittest 测试文件		# 示例如下
python -m unittest test_my_math.py		# 运行 test_my_math.py 文件

使用方式2时,如果没有指定测试用例,该命令会自动查找并运行当前目录下的所有测试用例。因此,也可直接使用下面的命令来运行当前目录下的所有测试用例:

python -m unittest

使用上面任意的方式来运行测试用例,都会得到如下的输出结果:

> python -m unittest test_my_math.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK

上面输出结果的第一行的两个点,每个点都代表一个测试用例(每个以 test_ 开头的方法都是一个真正独立的测试用例)的结果。在 test_my_math.py 文件有两个测试用例,因此会看到两个点,其中点代表测试用例通过。这一行除点之外,还有可能出现下面这些字符:

  • .:代表测试通过
  • F:代表测试失败,F 是 failure
  • E:测试出错,E 是 error
  • s:跳过该测试,s 是 skip。

上面输出的 ”Ran 2 tests in 0.001s“ 提示信息,说明本次测试运行了多少个测试用例,及测试花费的时间。接下来输出的 OK 表示所有测试用例均通过。

如果将 my_math.py 文件中的 one_equation() 函数的 return -b / a 修改为 return b / a,再次运行测试用例,会看到如下的输出结果:

> python -m unittest test_my_math.py
F.
======================================================================
FAIL: test_one_equation (test_my_math.TestMyModule)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_my_math.py", line 8, in test_one_equation
    self.assertEqual(one_equation(5, 9), -1.8)
AssertionError: 1.8 != -1.8

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

这次第一行的输出信息是 F.,说明第一个测试用例失败,第二个成功。两条横线之间是断言错误的 Traceback 信息,以及函数运行的实际输出结果和期望输出结果的差异。


3、使用测试包

使用测试包(TestSuite)可以组织多个测试用例,测试包还可以嵌套测试包。在使用测试包组织多个测试用例和测试包之后,就可以使用测试运行器(TestRunner)来运行该测试包所包含的所有测试用例。

为了使用测试包的功能,下面再写一个 hello.py 文件,代码如下:

def say_hello():
    return "Hello World."

def add(a, b):
    return a + b

接下来为 hello.py 文件提供测试类,测试文件是 test_hello.py,代码如下:

import unittest
from hello import *

class TestHello(unittest.TestCase):
    # 测试 say_hello 函数
    def test_say_hello(self):
        self.assertEqual(say_hello(), "Hello World.")
        self.assertNotEqual(say_hello(), "hello world.")
    # 测试 add 函数
    def test_add(self):
        self.assertEqual(add(3, 4), 7)
        self.assertEqual(add(0, 4), 4)
        self.assertEqual(add(-3, 0), -3)
        self.assertLess(add(5, 4), 10)

现在,在当前目录下有了 test_my_math.py 和 test_hello.py 两个文件,现在通过 TestSuite 将它们组织在一起,并使用 TestRunner 来运动测试包。组织后的文件名是 suite_test.py,代码如下:

import unittest
from test_my_math import TestMyModule
from test_hello import TestHello

test_cases = (TestHello, TestMyModule)
def whole_suite():
    # 创建测试加载器
    loader = unittest.TestLoader()
    # 创建测试包
    suite = unittest.TestSuite()
    # 遍历所有测试类
    for test_class in test_cases:
        # 从测试类中加载测试用例
        tests = loader.loadTestsFromTestCase(test_class)
        # 将测试用例添加到测试包中
        suite.addTests(tests)
    return suite

if __name__ == '__main__':
    # 创建测试运行器(TestRunner)
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(whole_suite())

上面这段代码中,通过调用 TestSuite 的 addTest() 方法添加测试用例,以此达到使用 TestSuite 来组织多个测试用例。此外,还使用了 TestLoader 对象来加载测试用例,该对象的 loadTestsFromTestCase() 方法从指定类加载测试用例。

在倒数第2行代码创建 TextTestRunner,这是一个测试运行器,专用于运行测试用例和测试包。前面使用 unittest.main() 函数,也是通过 TextTestRunner 来运行测试用例的。参数 verbosity=2 可以生成更详细的测试信息,这个参数在使用 unittest.main() 函数时同样可以指定。

运行 suite_test.py 文件,会生成如下测试报告。

test_add (test_hello.TestHello) ... ok
test_say_hello (test_hello.TestHello) ... ok
test_one_equation (test_my_math.TestMyModule) ... FAIL
test_two_equation (test_my_math.TestMyModule) ... ok

======================================================================
FAIL: test_one_equation (test_my_math.TestMyModule)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_my_math.py", line 8, in test_one_equation
    self.assertEqual(one_equation(5, 9), -1.8)
AssertionError: 1.8 != -1.8

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=1)

上面的测试报告以更详细的信息来提示每个测试用例的运行结果,在测试报告中仍然可以看到 test_one_equation() 测试失败。

如果要将测试报告输出到文件中,而不是显示在控制台,可以对 suite_test.py 文件的最后面的代码稍做修改,修改成如下形式:

if __name__ == '__main__':
    with open('my_test_report.txt', 'a') as f:
        # 创建测试运行器(TestRunner),将测试报告输出到文件中
        runner = unittest.TextTestRunner(verbosity=2, stream=f)
        runner.run(whole_suite())

这里主要是给 TextTestRunner() 方法指定 stream=f 参数来让测试报告输出到指定文件对象中。再次运行 suite_test.py 文件,此时在控制台中不会有任何输出,而是在当前目录下生成了 my_test_report.txt 的文件,测试报告就在这个文件中。


4、测试固件之 setUp 和 tearDown

前面使用了 unittest 模块的测试用例类(TestCase的子类)、测试包(TestSuite)和测试运行器TestRunner。此外,unittest 还有测试固件TestFixture。

  • 测试用例类:测试用例类就是单个的测试单元,其负责检查特定输入和对应的输出是否匹配。unittest 提供了一个TestCase 基类用于创建测试用例类。
  • 测试包: 用于组合多个测试用例,测试包也可以嵌套测试包。
  • 测试运行器: 负责组织、运行测试用例,并向用户呈现测试结果。
  • 测试固件: 代表执行一个或多个测试用例所需的准备工作,以及相关联的准备操作,准备工作可能包括创建临时数据库、创建目录、开启服务器进程等。

unittest.TestCase 包含有 setUp() 和 tearDown() 两个方法:

  • setUp()方法:初始化测试固件。在运行每个测试用例(以 test_ 开头的方法)之前自动执行 setUp() 方法来初始化测试固件。
  • tearDown()方法:销毁测试固件。在每个测试用例(以 test_ 开头的方法)运行完成后自动执行 tearDown() 方法来销毁测试固件。

所以,要为测试用例 初始化、销毁测试固件,只要重写 TestCase 的 setUp() 和 tearDown() 方法即可。示例如下,下面代码所在的文件名是 fixture_test1.py:

import unittest
from hello import *

class TestHello(unittest.TestCase):
    # 测试 say_hello 函数
    def test_say_hello(self):
        self.assertEqual(say_hello(), "Hello World.")
        self.assertNotEqual(say_hello(), "hello world.")

    # 测试 add 函数
    def test_add(self):
        self.assertEqual(add(3, 4), 7)
        self.assertEqual(add(0, 4), 4)
        self.assertEqual(add(-3, 0), -3)
        self.assertLess(add(5, 4), 10)

    def setUp(self):
        print('\n====执行 setUp 模拟初始化测试固件====')
    def tearDown(self):
        print('\n====调用 testDown 模拟销毁测试固件====')

在命令行执行下面命令运行 fixture_test1.py 文件:

python -m unittest -v fixture_test1.py

其中 -v 参数用于告诉 unittest 生成更详细的输出信息,输出信息如下:

test_add (fixture_test1.TestHello) ...
====执行 setUp 模拟初始化测试固件====

====调用 testDown 模块销毁测试固件====
ok
test_say_hello (fixture_test1.TestHello) ...
====执行 setUp 模拟初始化测试固件====

====调用 testDown 模拟销毁测试固件====
ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

上面输出结果可知,uittest 在运行每个测试用例(以 test_ 开头的方法)之前都执行了 setUp() 方法;在每个测试用例(以 test_ 开头的方法)运行完成之后都执行了 tearDown() 方法。

如果要在类(TestHello)的所有测试用例执行之前都用一个方法来初始化测试固件,在该类的所有测试用例执行之后都用一个方法来销毁测试固件,则可通过重写 setUpClass() 和 tearDownClass() 类方法来实现。示例如下,下面代码所在的文件名称是 fixture_test2.py :

import unittest
from hello import *

class TestHello(unittest.TestCase):
    # 测试 say_hello 函数
    def test_say_hello(self):
        self.assertEqual(say_hello(), "Hello World.")
        self.assertNotEqual(say_hello(), "hello world.")

    # 测试 add 函数
    def test_add(self):
        self.assertEqual(add(3, 4), 7)
        self.assertEqual(add(0, 4), 4)
        self.assertEqual(add(-3, 0), -3)
        self.assertLess(add(5, 4), 10)

    @classmethod
    def setUpClass(cls):
        print('\n====执行 setUpClass 在类级别模拟初始化测试固件====')
    @classmethod
    def tearDownClass(cls):
        print('\n====调用 testDownClass 在类级别模拟销毁测试固件====')

上面定义的 setUpClass() 和 tearDownClass() 两个类方法也是用于初始化测试固件和销毁测试固件的方法,但它们会在该类的所有测试用例执行之前和执行之后执行。在命令行测试结果如下:

> python -m unittest -v fixture_test2.py

====执行 setUpClass 在类级别模拟初始化测试固件====
test_add (fixture_test2.TestHello) ... ok
test_say_hello (fixture_test2.TestHello) ... ok

====调用 testDownClass 在类级别模拟销毁测试固件====

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

5、跳过测试用例

如果要跳过某个测试用例,有两种方式可以实现:

  • 方式1:使用 skipXxx 装饰器来跳过测试用例。uittest 模块提供了3个装饰器,分别是:
  1. @unittest.skip(reason):无条件跳过,reason 是一个字符串;
  2. @unittest.skipIf(condition, reason):当 condition 为 True 时跳过;
  3. @unittest.skipUnless(condition, reason):当 condition 为 False 跳过。
  • 方式2:使用 TestCase 的 skipTest() 方法来跳过测试用例。

下面代码使用@unittest.skip 装饰器来跳过测试用例。代码所在的文件名是 skip_test1.py 。

import unittest
from hello import *

class TestHello(unittest.TestCase):
    # 测试 say_hello 函数
    def test_say_hello(self):
        self.assertEqual(say_hello(), "Hello World.")
        self.assertNotEqual(say_hello(), "hello world.")
    # 测试 add 函数
    @unittest.skip("临时跳过 test_add")
    def test_add(self):
        self.assertEqual(add(3, 4), 7)
        self.assertEqual(add(0, 4), 4)
        self.assertEqual(add(-3, 0), -3)
        self.assertLess(add(5, 4), 10)

在命令行执行 skip_test1.py 文件的输出结果如下:

> python -m unittest skip_test1.py
s.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK (skipped=1)

输出的第一行是 s.,说明有两个测试用例,其中 s 表示跳过第一个测试用例,点(.) 表示第二个测试用例通过。

第2种方式,下面使用 TestCase 的 skipTest() 方法跳过测试用例,所在的文件名称是 skip_test2.py。

import unittest
from hello import *

class TestHello(unittest.TestCase):
    # 测试 say_hello 函数
    def test_say_hello(self):
        self.assertEqual(say_hello(), "Hello World.")
        self.assertNotEqual(say_hello(), "hello world.")
    # 测试 add 函数
    def test_add(self):
        self.skipTest("临时跳过 test_add")	# 跳过这个测试用例
        self.assertEqual(add(3, 4), 7)
        self.assertEqual(add(0, 4), 4)
        self.assertEqual(add(-3, 0), -3)
        self.assertLess(add(5, 4), 10)

在命令行执行 skip_test2.py 文件的输出结果如下:

> python -m unittest -v skip_test2.py
test_add (skip_test2.TestHello) ... skipped '临时跳过 test_add'
test_say_hello (skip_test2.TestHello) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK (skipped=1)

使用 -v 选项可以生成更详细的测试报告。输出结果显示了跳过的原因,这是因为使用了 -v 选项。


五、小结

  • Python 开发相关的附属知识:文档和测试。
  • 在实际开发中,文档是非常重要的,如果没有有效的说明文档,对其他人来说就不能有效地使用该程序。
  • pydoc 工具可查看、生成文档,也可生成HTML文档。只要为模块提供了符合格式的文档说明。
  • Python 的两种测试:文档测试和单元测试。
  • 文档测试是传统的测试方式,优点:简单、易用,但在工程化方面略有欠缺。
  • 单元测试工具unittest,属于 xUnit 单元测试家族的一员,需要重点掌握。
posted @ 2020-07-23 15:36  远方那一抹云  阅读(437)  评论(0编辑  收藏  举报