Python unittest - zhongjiajie/zhongjiajie.github.com GitHub Wiki

Python 单元测试

提高你的Python能力:理解单元测试中可以了解到,测试不仅能看代码是不是有错 还可以看代码之前是否有考虑不全的地方,反馈使得代码更加好,所以完成程序之后增加测试用例是需要且必要的。

unittest中最核心的四个概念是:test case, test suite, test runner, test fixture

下图是unittest的静态类图

/img/python-unittest.png

一个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')

运行unittest

python -m unittest discover <test_directory>
# or
python -m unittest discover -s <directory> -p '*_test.py'

FAQ

如果外部依赖不满足跳过单元测试

如果单元测试是对可选库的测试,应该要验证库是否存在,如果不存在则跳过该单元测试,例如这个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)

unittest怎么判断list是否相等

使用如果判断两个列表是否相等+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)

unittest测试日志

这里

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

⚠️ **GitHub.com Fallback** ⚠️