Python unittest - zhongjiajie/zhongjiajie.github.com GitHub Wiki
从提高你的Python能力:理解单元测试中可以了解到,测试不仅能看代码是不是有错 还可以看代码之前是否有考虑不全的地方,反馈使得代码更加好,所以完成程序之后增加测试用例是需要且必要的。
unittest中最核心的四个概念是:test case
, test suite
, test runner
, test fixture
下图是unittest的静态类图
一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。 测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
而对一个测试用例环境的搭建和销毁,是一个fixture。
一个class继承了unittest.TestCase,便是一个测试用例,但如果其中有多个以 test 开头的方法,那么每有一个这样的方法,在load的时候便会生成一个TestCase实例,如:一个class中有四个test_xxx方法,最后在load到suite中时也有四个测试用例。
到这里整个流程就清楚了:
写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。这里加个说明,在Runner执行时,默认将执行结果输出到控制台,我们可以设置其输出到文件,在文件中查看结果(你可能听说过HTMLTestRunner,是的,通过它可以将结果输出到HTML中,生成漂亮的报告,它跟TextTestRunner是一样的,从名字就能看出来,这个我们后面再说)。
Basic example
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()
TestCase assert methods
Method | Checks that | New in |
---|---|---|
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 | 3.1 |
assertIsNot(a, b) | a is not b | 3.1 |
assertIsNone(x) | x is None | 3.1 |
assertIsNotNone(x) | x is not None | 3.1 |
assertIn(a, b) | a in b | 3.1 |
assertNotIn(a, b) | a not in b | 3.1 |
assertIsInstance(a, b) | isinstance(a, b) | 3.2 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 3.2 |
unittest exceptions, warnings, and log messages
Method | Checks that | New in |
---|---|---|
Method | Checks that | New in |
assertRaises(exc, fun, *args, **kwds) | fun(*args, **kwds) raises exc | |
assertRaisesRegex(exc, r, fun, *args, **kwds) | fun(*args, **kwds) raises exc and the message matches regex r | 3.1 |
assertWarns(warn, fun, *args, **kwds) | fun(*args, **kwds) raises warn | 3.2 |
assertWarnsRegex(warn, r, fun, *args, **kwds) | fun(*args, **kwds) raises warn and the message matches regex r | 3.2 |
assertLogs(logger, level) | The with block logs on logger with minimum level | 3.4 |
TestCase specific checks
Method | Checks that | New in |
---|---|---|
assertAlmostEqual(a, b) | round(a-b, 7) == 0 | |
assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 | |
assertGreater(a, b) | a > b | 3.1 |
assertGreaterEqual(a, b) | a >= b | 3.1 |
assertLess(a, b) | a < b | 3.1 |
assertLessEqual(a, b) | a <= b | 3.1 |
assertRegex(s, r) | r.search(s) | 3.1 |
assertNotRegex(s, r) | not r.search(s) | 3.2 |
assertCountEqual(a, b) | a and b have the same elements in the same number, regardless of their order | 3.2 |
further example
import unittest
class WidgetTestCase(unittest.TestCase):
# 处理单元测试的初始化环境和删除单元测试环境
@classmethod
def setUpClass(cls):
print "This setUpClass() method only called once."
@classmethod
def tearDownClass(cls):
print "This tearDownClass() method only called once too."
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
# 通过装饰器跳过单元测试的部分测试用例
@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") # 条件为True跳过
def test_format(self):
# Tests that work for only a certain version of the library.
pass
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") # 条件为False跳过
def test_windows_support(self):
# windows specific testing code
pass
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')
- 单独运行每一个测试用例,直接在ide或者文本编辑器中运行单个文件
- 运行文件夹下面所有的测试用例,参照stackoverflow:how do i run all python unittests in a directory以及unittest-offcial
python -m unittest discover <test_directory>
# or
python -m unittest discover -s <directory> -p '*_test.py'
如果单元测试是对可选库的测试,应该要验证库是否存在,如果不存在则跳过该单元测试,例如这个review
import unittest
try:
import cx_Oracle
except ImportError:
cx_Oracle = None
@unittest.skipIf(cx_Oracle is None, 'cx_Oracle package not present')
class Test...(unittest.TestCase):
...
如果需要对日志进行测试,或者测试的变量在日志中,可以使用unittest.Testcase.assertLogs
with self.assertLogs('foo', level='INFO') as cm:
logging.getLogger('foo').info('first message')
logging.getLogger('foo.bar').error('second message')
self.assertEqual(cm.output, ['INFO:foo:first message',
'ERROR:foo.bar:second message'])
# 如果是很长的,而且是非原生的log,可以使用assertIn
self.assertIn('INFO:foo:first message', cm.output)
使用如果判断两个列表是否相等+assertEqual
l1 = [a,b]
l2 = [b,a]
# python >= 3.0
self.assertCountEqual(l1, l2)
# python >= 2.7
self.assertItemsEqual(l1, l2)
# both py2 and py3
import six
six.assertCountEqual(self, l1, l2)
对于python3,列表是否相等有两种情况self.assertCountEqual
仅对比列表的元素是否相等,self.assertListEqual
除了对比列表的元素是否相等外,还有对比元素的顺序是否相等,详情看这里
import unittest
class TestListElements(unittest.TestCase):
def setUp(self):
self.expected = ['foo', 'bar', 'baz']
self.result = ['baz', 'foo', 'bar']
def test_count_eq(self):
"""Will succeed"""
self.assertCountEqual(self.result, self.expected)
def test_list_eq(self):
"""Will fail"""
self.assertListEqual(self.result, self.expected)
unittest.TestCase.assertRaises
,测试某个可能出现的异常
import mymod
class MyTestCase(unittest.TestCase):
def test_myfunc_raise(self):
self.assertRaises(SomeCoolException, mymod.myfunc)
import io
import unittest
import unittest.mock
from .solution import fizzbuzz
class TestFizzBuzz(unittest.TestCase):
@unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
def assert_stdout(self, n, expected_output, mock_stdout):
fizzbuzz(n)
self.assertEqual(mock_stdout.getvalue(), expected_output)
def test_only_numbers(self):
self.assert_stdout(2, '1\n2\n')
多个雷同的单元测试是非常常见的情况,比如我们要先测试默认self.db_hook
默认参数和指定参数{'database': 'abc'}
,这里仅仅是参数不一样,测试逻辑都一样,对于这样的测试之前的使用方式是分成两大块,将两组参数和对应的逻辑都放到单元测试中
def test_database(self):
params = {'database': 'abc'}
hook = self.get_hook(**params)
assert hook.database == 'abc'
params = {}
hook = self.get_hook(**params)
assert hook.database == 'schema'
现在我们可以使用parameterized
简化这样的操作,如下。这样做的好处的意图更加清晰,可知是对同样的逻辑选用不同的参数测试,单元测试代码更加简洁。注意这个会重命名test,在函数名后面增加_number
from parameterized import parameterized
@parameterized.expand([
({'database': 'abc'}, 'abc'),
({}, 'schema'),
])
def test_database(self, hook_params, db):
hook = self.db_hook(**hook_params)
assert hook.database == db