1、unittest 执行顺序以及共享数据的问题
unittest 是 python 官方的单元测试工具。最近发现一个之前没注意到的盲区:
unittetst 的每个测试用例都会重建一个 TestCase 类,因此会多次初始化,并且每次测试是 self 都指向独立的实例。
这样在测试用例之间共享数据,需要特别留意。如果数据只读还好,涉及到修改,大概率不符合预期。
例子:
import unittest
class TestClass(unittest.TestCase):
num = 1
def test_foo(self):
self.assertEqual(self.num, 1)
self.num = 2
def test_abc(self):
self.assertEqual(self.num, 2)
if __name__ == "__main__":
unittest.main()
一眼看上去上面的代码没有问题。实际问题很大:
- unittest 同一个 TestCase 下的不同测试用例(即
test_
成员函数),不是按照定义顺序运行,而是按照字母排序执行!这意味着test_abc
在test_foo
之前执行。 test_abc
和test_foo
事实上是不同的TestClass
实例,这意味着test_foo
里面的self.num=2
的修改,不会被别的测试用例看到(即使是在它之后执行的测试用例)。
2、unittest 执行测试的逻辑
如果了解 unittest 的测试逻辑,就能理解上面这些问题:
- 导入所有的 test 文件,通常是指定的单个文件,或者 discover 查找到的所有 test 文件(指定目录下以 test 开头的 python 文件)。
- 所有类下面直接定义的类变量和语句将直接执行(比如上面的
num = 1
)。 - 开始依次执行所有的 TestCase (是指继承 unittest.TestCase 且以 Test 开头的类):
- 先执行该类的
setUpClass
函数(该函数只会被执行一次)。 - 按字母顺序执行测试用例,对每个测试用例生成一个独立的类实例:
- 并顺序执行
__init__
,setUp
,当前测试函数(以test
开头),tearDown
- 在有多个测试函数时,
__init__
、setUp
、tearDown
会被执行多次。
- 并顺序执行
- 执行该类的
tearDownClass
函数(该函数只会被执行一次)
- 先执行该类的
从逻辑上看,不同 TestCase 之间的环境肯定是完全独立的。但 unittest 推荐测试用例的环境,也是完全独立的。
不推荐定义__init__
函数,因为基类的初始化函数参数是未知的(实际测试发现初始化参数是测试用例函数的名字)。初始化可以放在setUp
函数里面。
如果实在想写,必须下面这样:
class TestClass(unittest.TestCase):
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
print("init", args, kwargs)
3、如何共享数据
不同的测试用例之间共享一个不会被修改的数据,可以通过类成员变量来实现,一个最简单的就是上面示例中的num = 1
,但需要注意后面的测试用例不能用self.num = 2
来修改(该修改将只影响到该测试用例,其它测试用例的 num 仍等于 1 )。这个原因是,一旦重新定义self.num
,只是重新定义了一个实例成员数据,覆盖了类成员数据。其它测试用例将重建一个类实例,看不到现在这个实例的成员数据。
class TestClass(unittest.TestCase):
num = 1
def test_foo(self):
self.assertEqual(self.num, 1)
def test_abc(self):
self.assertEqual(self.num + 1, 2)
一个更符合工程质量的方法是使用setUpClass
和tearDownClass
。
4、如何共享且修改数据?
一般不建议共享会被修改的数据,因为如上所示,测试用例的执行并不是按照定义的顺序从上到下执行。执行顺序可能和预期不一样,从而导致错误。
如果非要做,只能使用一个 dict 或者 list 来修改,通过修改 dict/list 内部的数据:
class TestClass(unittest.TestCase):
self.shared = {
"num": 1
}
def test_1(self):
self.assertEqual(self.shared["num"], 1)
self.shared["num"] = 2
def test_2(self):
self.assertEqual(self.shared["num"], 2)
5、如何组织测试文件
一个文件里可以定义多个 TestCase ,每个 TestCase 下面可以定义多个测试用例(每个测试用例是一个test
开头没有多余参数的类成员函数,其它名字的函数将会被忽略)。
文件的最后注意加上:
if __name__ == "__main__":
unittest.main()
如果单个文件就能放下所有测试案例,那么直接命令行就能执行测试:
python ./your_test_file.py -v
其中-v
参数可以显示测试明细和进度。
如果有多个文件,我们假设它们放在同一个目录./my_tests
下,
python -m unittest discover -s ./my_tests
也可以写一个./my_tests/alltests.py
的脚本:
#!/usr/bin/env python
# encoding: utf-8
import unittest
import os
def test_all():
dir_path = os.path.dirname(os.path.realpath(__file__))
discover = unittest.defaultTestLoader.discover(dir_path, pattern='test_*.py')
return discover
def __main():
runner = unittest.TextTestRunner(verbosity=2)
res = runner.run(test_all())
print(res)
if res.errors or res.failures:
# do with errors and failures
pass
if __name__ == "__main__":
__main()
然后执行该脚本:
./my_tests/alltests.py
这样做的好处是,可以对测试出现的错误做后续处理,比如发送邮件通知。
6、测试函数
下面这些测试函数,都可以再携带一个 msg 参数,当出错时打印相应的错误信息。
普通检查:
- 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 is not b
- assertIsNone: a is None
- assertIsNotNone(a): a is not None
- assertIn(a, b): a in b
- assertNotIn(a, b): a not in b
- assertCountEqual(a, b): sorted(a) == sorted(b)
- assertIsInstance(a, b): isinstance(a, b)
- assertNotIsInstance(a, b): not isinstance(a, b)
- assertGreater(a, b): a > b
- assertGreaterEqual(a, b): a >= b
- assertLess(a, b): a < b
- assertLessEqual(a, b): a <= b
注意优先使用 assertEqual(a, b),而不是 assertTrue(a == b),这是因为出错时,前者会能提示 a 和 b 具体的值。
浮点数检查(浮点数的检查和比较还是较为粗略,实际使用过程需要特别留意):
- assertAlmostEqual(a, b, places=7): round(a - b, places) == 0
- assertNotAlmostEqual(a, b, places=7): round(a - b, places) != 0
文本相关:
- assertRegex(s, r): r.search(s)
- assertNotRegex(s, r): not r.search(s)
异常检查:
assertRaises(exc, fun, *args, **kwargs)
: 执行fun(*args, **kwargs)
会引发 exc 的异常。
Q. E. D.