python standard library unittest

python 标准库 unittest

今天又学到一种单词记忆方法

https://www.wikihow.com/Memorize-Words-Quickly

基本原则我梳理了一下:

  1. Using Word Association and Mnemonic Devices

  2. Visualizing Words

  3. Using Sound and Music

源代码: Lib/unittest/init.py


(如果你已经对测试的概念比较熟悉了,你可能想直接跳转到这一部分 断言方法。)

unittest 单元测试框架是受到 JUnit 的启发,与其他语言中的主流单元测试框架有着相似的风格。其支持测试自动化,配置共享和关机代码测试。支持将测试样例聚合到测试集中,并将测试与报告框架独立。

为了实现这些,unittest 通过面向对象的方式支持了一些重要的概念。

  • 测试脚手架

    test fixture 表示为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。

  • 测试用例

    一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。 unittest 提供一个基类: TestCase ,用于新建测试用例。

  • 测试套件

    test suite 是一系列的测试用例,或测试套件,或两者皆有。它用于归档需要一起执行的测试。

  • 测试运行器(test runner)

    test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。

参见

Tools/unittestgui/unittestgui.py脚本是用来做探索测试和执行的GUI工具。这主要是为了更加方便那些新的单元测试用例。对于生产环境而言,建议还是使用例如 Buildbot, Jenkins or Hudson 这样的持续集成工具来执行测试。

基本实例

unittest 模块提供了一系列创建和运行测试的工具。这一段落演示了这些工具的一小部分,但也足以满足大部分用户的需求。

这是一段简短的代码,来测试三种字符串方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unittest

class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())

def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)

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

继承 unittest.TestCase 就创建了一个测试样例。上述三个独立的测试是三个类的方法,这些方法的命名都以 test 开头。 这个命名约定告诉测试运行者类的哪些方法表示测试。

每个测试的关键是:调用 assertEqual() 来检查预期的输出; 调用 assertTrue()assertFalse() 来验证一个条件;调用 assertRaises() 来验证抛出了一个特定的异常。使用这些方法而不是 assert 语句是为了让测试运行者能聚合所有的测试结果并产生结果报告。

通过 setUp()tearDown() 方法,可以设置测试开始前与完成后需要执行的指令。 在 组织你的测试代码 中,对此有更为详细的描述。

最后的代码块中,演示了运行测试的一个简单的方法。 unittest.main() 提供了一个测试脚本的命令行接口。当在命令行运行该测试脚本,上文的脚本生成如以下格式的输出:

1
2
3
4
5
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

在调用测试脚本时添加 -v 参数使 unittest.main() 显示更为详细的信息,生成如以下形式的输出:

1
2
3
4
5
6
7
8
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

以上例子演示了 unittest 中最常用的、足够满足许多日常测试需求的特性。文档的剩余部分详述该框架的完整特性。

命令行界面

unittest 模块可以通过命令行运行模块、类和独立测试方法的测试:

1
2
3
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

你可以传入模块名、类或方法名或他们的任意组合。

同样的,测试模块可以通过文件路径指定:

1
python -m unittest tests/test_something.py

这样就可以使用 shell 的文件名补全指定测试模块。所指定的文件仍需要可以被作为模块导入。路径通过去除 ‘.py’ 、把分隔符转换为 ‘.’ 转换为模块名。若你需要执行不能被作为模块导入的测试文件,你需要直接执行该测试文件。

在运行测试时,你可以通过添加 -v 参数获取更详细(更多的冗余)的信息。

1
python -m unittest -v test_module

当运行时不包含参数,开始 探索性测试

1
python -m unittest

用于获取命令行选项列表:

1
python -m unittest -h

在 3.2 版更改: 在早期版本中,只支持运行独立的测试方法,而不支持模块和类。

命令行选项

unittest 支持下面的命令行选项:

  • -b, --buffer

    在测试运行时,标准输出流与标准错误流会被放入缓冲区。成功的测试的运行时输出会被丢弃;测试不通过时,测试运行中的输出会正常显示,错误会被加入到测试失败信息。

  • -c, --catch

    当测试正在运行时, Control-C 会等待当前测试完成,并在完成后报告已执行的测试的结果。当再次按下 Control-C 时,引发平常的 KeyboardInterrupt 异常。See Signal Handling for the functions that provide this functionality.

  • -f, --failfast

    当出现第一个错误或者失败时,停止运行测试。

  • -k

    只运行匹配模式或子串的测试方法和类。可以多次使用这个选项,以便包含匹配子串的所有测试用例。包含通配符(*)的模式使用 fnmatch.fnmatchcase() 对测试名称进行匹配。另外,该匹配是大小写敏感的。模式对测试加载器导入的测试方法全名进行匹配。例如,-k foo 可以匹配到 foo_tests.SomeTest.test_somethingbar_tests.SomeTest.test_foo ,但是不能匹配到 bar_tests.FooTest.test_something

  • --locals

    在回溯中显示局部变量。

3.2 新版功能: 添加命令行选项 -b, -c-f

3.5 新版功能: 命令行选项 --locals

3.7 新版功能: 命令行选项 -k

命令行亦可用于探索性测试,以运行一个项目的所有测试或其子集。

探索性测试

3.2 新版功能.

Unittest支持简单的测试搜索。若需要使用探索性测试,所有的测试文件必须是 modulespackages (包括 namespace packages )并可从项目根目录导入(即它们的文件名必须是有效的 identifiers )。

探索性测试在 TestLoader.discover() 中实现,但也可以通过命令行使用。它在命令行中的基本用法如下:

1
2
cd project_directory
python -m unittest discover

方便起见, python -m unittestpython -m unittest discover 等价。如果你需要向探索性测试传入参数,必须显式地使用 discover 子命令。

discover 有以下选项:

  • -v, --verbose

    更详细地输出结果。

  • -s, --start-directory directory

    开始进行搜索的目录(默认值为当前目录 . )。

  • -p, --pattern pattern

    用于匹配测试文件的模式(默认为 test*.py )。

  • -t, --top-level-directory directory

    指定项目的最上层目录(通常为开始时所在目录)。

-s-p-t 选项可以按顺序作为位置参数传入。以下两条命令是等价的:

1
2
python -m unittest discover -s project_directory -p "*_test.py"
python -m unittest discover project_directory "*_test.py"

正如可以传入路径那样,传入一个包名作为起始目录也是可行的,如 myproject.subpackage.test 。你提供的包名会被导入,它在文件系统中的位置会被作为起始目录。

探索性测试通过导入测试对测试进行加载。在找到所有你指定的开始目录下的所有测试文件后,它把路径转换为包名并进行导入。如 foo/bar/baz.py 会被导入为 foo.bar.baz

如果你有一个全局安装的包,并尝试对这个包的副本进行探索性测试,可能会从错误的地方开始导入。如果出现这种情况,测试会输出警告并退出。

如果你使用包名而不是路径作为开始目录,搜索时会假定它导入的是你想要的目录,所以你不会收到警告。

测试模块和包可以通过 load_tests protocol 自定义测试的加载和搜索。

在 3.4 版更改: 探索性测试支持命名空间包( namespace packages )。

组织你的测试代码

单元测试的构建单位是 test cases :独立的、包含执行条件与正确性检查的方案。在 unittest 中,测试用例表示为 unittest.TestCase 的实例。通过编写 TestCase 的子类或使用 FunctionTestCase 编写你自己的测试用例。

一个 TestCase 实例的测试代码必须是完全自含的,因此它可以独立运行,或与其它任意组合任意数量的测试用例一起运行。

TestCase 的最简单的子类需要实现一个测试方法(例如一个命名以 test 开头的方法)以执行特定的测试代码:

1
2
3
4
5
6
import unittest

class DefaultWidgetSizeTestCase(unittest.TestCase):
def test_default_widget_size(self):
widget = Widget('The widget')
self.assertEqual(widget.size(), (50, 50))

可以看到,为了进行测试,我们使用了基类 TestCase 提供的其中一个 assert*() 方法。若测试不通过,将会引发一个带有说明信息的异常,并且 unittest 会将这个测试用例标记为测试不通过。任何其它类型的异常将会被当做错误处理。

可能同时存在多个前置操作相同的测试,我们可以把测试的前置操作从测试代码中拆解出来,并实现测试前置方法 setUp() 。在运行测试时,测试框架会自动地为每个单独测试调用前置方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import unittest

class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')

def test_default_widget_size(self):
self.assertEqual(self.widget.size(), (50,50),
'incorrect default size')

def test_widget_resize(self):
self.widget.resize(100,150)
self.assertEqual(self.widget.size(), (100,150),
'wrong size after resize')

多个测试运行的顺序由内置字符串排序方法对测试名进行排序的结果决定。

在测试运行时,若 setUp() 方法引发异常,测试框架会认为测试发生了错误,因此测试方法不会被运行。

相似的,我们提供了一个 tearDown() 方法在测试方法运行后进行清理工作。

1
2
3
4
5
6
7
8
import unittest

class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')

def tearDown(self):
self.widget.dispose()

setUp() 成功运行,无论测试方法是否成功,都会运行 tearDown()

这样的一个测试代码运行的环境被称为 test fixture 。一个新的 TestCase 实例作为一个测试脚手架,用于运行各个独立的测试方法。在运行每个测试时,setUp()tearDown()__init__() 会被调用一次。

建议使用TestCase来实现根据测试实现的功能将测试组合在一起。·单元测试通机制,在一个测试集中的测试套件中组织的方式。

通常情况,调用unittest.main()来执行测试。

然而,如果你需要自定义你的测试套件的话,你可以参考以下方法组织你的测试:

1
2
3
4
5
6
7
8
9
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase('test_default_widget_size'))
suite.addTest(WidgetTestCase('test_widget_resize'))
return suite

if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())

您可以将测试用例和测试套件的定义与它们要测试的代码放在相同的模块中(例如“ widget.py”),但是将测试代码放置在独立的模块中有很多优点,例如“ test_widget.py`:

你可以遵循以下原则:

  • 测试模块可以通过命令行方式单独运行
  • 测试脚本可以很容易被隔离
  • 在没有充分理由的情况下,尽量不要为了适配代码而修改测试代码。
  • 测试代码的修改频率应低于其测试的代码。
  • 测试的代码可以更容易地重构。
  • 无论如何用c编写的模块都必须在一个模块中隔离,为什么必须要一致呢?
  • 如果测试策略更改了,没必要去修改测试代码

复用已有的测试代码

一些用户希望直接使用 unittest 运行已有的测试代码,而不需要把已有的每个测试函数转化为一个 TestCase 的子类。

因此, unittest 提供 FunctionTestCase 类。这个 TestCase 的子类可用于打包已有的测试函数,并支持设置前置与后置函数。

假定有一个测试函数:

1
2
3
4
def testSomething():
something = makeSomething()
assert something.name is not None
# ...

可以创建等价的测试用例如下,其中前置和后置方法是可选的。

1
2
3
testcase = unittest.FunctionTestCase(testSomething,
setUp=makeSomethingDB,
tearDown=deleteSomethingDB)

尽管 FunctionTestCase 可以快速将一个已经存在的测试转换为基于unittest的系统,但是不推荐这么做。好好设计TestCase子类将会在未来重构代码变得更容易。

在特定情况下,现有的测试可以使用 doctest 模块。如果是那样, 会绑定DocTestSuite 类从已经存在的 unittest.TestSuite 自动构建 基于doctest 实例。

忽略测试与预计的失败

3.1 新版功能.

Unittest 支持忽略测试方法或者忽略整个测试。除此以外,还支持标注某个测试”强制失败”,但是在TestResult 中不会体现这类测试为失败。

忽略测试可以使用 skip() decorator 装饰器,或者其中一些条件变量,直接添加SkipTest 注解

忽略测试的基本用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyTestCase(unittest.TestCase):

@unittest.skip("demonstrating skipping")
def test_nothing(self):
self.fail("shouldn't happen")

@unittest.skipIf(mylib.__version__ < (1, 3),
"not supported in this library version")
def test_format(self):
# Tests that work for only a certain version of the library.
pass

@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_windows_support(self):
# windows specific testing code
pass

def test_maybe_skipped(self):
if not external_resource_available():
self.skipTest("external resource not available")
# test code that depends on the external resource
pass

在详细测试模式下运行以上测试例子时,程序输出如下:

1
2
3
4
5
6
7
8
9
test_format (__main__.MyTestCase) ... skipped 'not supported in this library version'
test_nothing (__main__.MyTestCase) ... skipped 'demonstrating skipping'
test_maybe_skipped (__main__.MyTestCase) ... skipped 'external resource not available'
test_windows_support (__main__.MyTestCase) ... skipped 'requires Windows'

----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK (skipped=4)

忽略测试类的写法跟忽略测试方法的写法相似:

1
2
3
4
@unittest.skip("showing class skipping")
class MySkippedTestCase(unittest.TestCase):
def test_not_run(self):
pass

TestCase.setUp() 也可以忽略测试。可以用于所需资源不可用的情况下忽略接下来的测试。

使用 expectedFailure() 装饰器表明这个测试预计失败。:

1
2
3
4
class ExpectedFailureTestCase(unittest.TestCase):
@unittest.expectedFailure
def test_fail(self):
self.assertEqual(1, 0, "broken")

通过测试方法中调用 skip()装饰器,可以轻松忽略那些希望被忽略的测试。除非传递的对象具有某个属性,否则此装饰器将起作用,如下面的例子:

1
2
3
4
def skipUnlessHasattr(obj, attr):
if hasattr(obj, attr):
return lambda func: func
return unittest.skip("{!r} doesn't have {!r}".format(obj, attr))

下面的装饰器和异常处理可以直接忽略期望失败的测试:

  • `@unittest.skip`(reason)

    忽略被此装饰器装饰的测试。 reason 为测试被忽略的原因。

  • `@unittest.skipIf`(condition, reason)

    condition 为真时,忽略被装饰的测试。

  • `@unittest.skipUnless`(condition, reason)

    忽略被装饰的测试,除非 condition 为真。

  • `@unittest.expectedFailure`

    把测试标记为预计失败。如果测试不通过,会被认为测试成功;如果测试通过了,则被认为是测试失败。

  • exception unittest.SkipTest(reason)

    引发此异常以忽略一个测试。通常来说,你可以使用 TestCase.skipTest() 或其中一个忽略测试的装饰器实现忽略测试的功能,而不是直接引发此异常。

被忽略的测试的 setUp()tearDown() 不会被运行。被忽略的类的 setUpClass()tearDownClass() 不会被运行。被忽略的模组的 setUpModule()tearDownModule() 不会被运行。

使用 subtest 执行Distinguishing test迭代

3.4 新版功能.

如果测试之间的差异很小,例如某些参数,则unittest允许您使用subTest() 的上下文装饰器

例如,以下测试:

1
2
3
4
5
6
7
8
9
class NumbersTest(unittest.TestCase):

def test_even(self):
"""
Test that numbers between 0 and 5 are all even.
"""
for i in range(0, 6):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)

可以得到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

如果不使用子测试,则在第一次失败后执行将停止,并且错误将不那么容易诊断,因为不会显示i的值:

1
2
3
4
5
6
7
======================================================================
FAIL: test_even (__main__.NumbersTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
坚持原创技术分享,您的支持将鼓励我继续创作!