Python 单元测试

发布于 2024-11-30 12:49:51 字数 10824 浏览 26 评论 0

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

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

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

MethodChecks thatNew 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 b3.1
assertIsNot(a, b)a is not b3.1
assertIsNone(x)x is None3.1
assertIsNotNone(x)x is not None3.1
assertIn(a, b)a in b3.1
assertNotIn(a, b)a not in b3.1
assertIsInstance(a, b)isinstance(a, b)3.2
assertNotIsInstance(a, b)not isinstance(a, b)3.2

unittest exceptions, warnings, and log messages

MethodChecks thatNew in
MethodChecks thatNew 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 r3.1
assertWarns(warn, fun, *args, **kwds)fun(*args, **kwds) raises warn3.2
assertWarnsRegex(warn, r, fun, *args, **kwds)fun(*args, **kwds) raises warn and the message matches regex r3.2
assertLogs(logger, level)The with block logs on logger with minimum level3.4

TestCase specific checks

MethodChecks thatNew in
assertAlmostEqual(a, b)round(a-b, 7) == 0 
assertNotAlmostEqual(a, b)round(a-b, 7) != 0 
assertGreater(a, b)a > b3.1
assertGreaterEqual(a, b)a >= b3.1
assertLess(a, b)a < b3.1
assertLessEqual(a, b)a <= b3.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 order3.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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

中二柚

暂无简介

文章
评论
26 人气
更多

推荐作者

迎风吟唱

文章 0 评论 0

qq_hXErI

文章 0 评论 0

茶底世界

文章 0 评论 0

捎一片雪花

文章 0 评论 0

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文