单元测试的概念
随着软件开发规模的不断变大,代码体积的膨胀,路径覆盖,以及整体测试会变得十分困难
所以错误定位,以及 代码单元可靠 十分必要。
单元测试,简单讲,就是对 一部分代码编写测试用例,以确保被测试的代码单元代码的可靠性。
下边举一个很简单的单元测试例子
待测试单元(模块)
my_math.py
很简单的模块,他只包含了,加法,乘法,阶乘
#!/usr/bin/python # -*- coding:utf-8 -*- ''' @File : my_math.py @Time : 2020/12/24 16:38:24 @Author : lmk @Version : 1.0 ''' def my_add(a, b): return a+b def my_multiply(a, b): return a*b def my_factorial(a, b): return a**b if __name__ == '__main__': pass
编写一个 加法 测试单元
#!/usr/bin/python # -*- coding:utf-8 -*- ''' @File : test_my_math.py @Time : 2020/12/24 16:26:49 @Author : lmk @Version : 1.0 ''' from testtools import TestCase import my_math class Test_my_math(TestCase): def setUp(self): super(Test_my_math, self).setUp() print("this is set_up") def test_my_add_case_1(self): self.assertEqual(6, my_math.my_add(1, 5)) def test_my_add_case_2(self): self.assertEqual(7, my_math.my_add(1, 5)) if __name__ == '__main__': pass
执行这个测试单元
python3 -m testtools.run test_my_math.py RuntimeWarning: 'testtools.run' found in sys.modules after import of package 'testtools', but prior to execution of 'testtools.run'; this may result in unpredictable behaviour warn(RuntimeWarning(msg)) Tests running... this is set_up this is set_up ====================================================================== FAIL: test_my_math.Test_my_math.test_my_add_case_2 ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/lmk/codes/learn_testtools/test_my_math.py", line 25, in test_my_add_case_2 self.assertEqual(7, my_math.my_add(1, 5)) File "/home/lmk/.local/lib/python3.6/site-packages/testtools/testcase.py", line 415, in assertEqual self.assertThat(observed, matcher, message) File "/home/lmk/.local/lib/python3.6/site-packages/testtools/testcase.py", line 502, in assertThat raise mismatch_error testtools.matchers._impl.MismatchError: 7 != 6 Ran 2 tests in 0.002s FAILED (failures=1)
Ran 2 tests in 0.002s
FAILED (failures=1)
输出结果表明 两项 测试 有一项失败了
assertEqual 断言函数,接受三个参数
def assertEqual(self, expected, observed, message=’’):
expected 是 逻辑上期待 的正确结果
observed 是 代码实际 的运行结果
message 是 额外的 错误信息
测试命令的其他可选项
python3 -m testtools.run -h -h, --help show this help message and exit -v, --verbose Verbose output -q, --quiet Quiet output --locals Show local variables in tracebacks -f, --failfast Stop on first fail or error -c, --catch Catch ctrl-C and display results so far -b, --buffer Buffer stdout and stderr during tests -l, --list List tests rather than executing them --load-list LOAD_LIST Specifies a file containing test ids, only tests matching those ids are executed -s START, --start-directory START Directory to start discovery ('.' default) -p PATTERN, --pattern PATTERN Pattern to match tests ('test*.py' default) -t TOP, --top-level-directory TOP Top level directory of project (defaults to start directory)
进阶1 - mock 模拟测试
在开发工作中,难免有一些函数,项目是 并行开发的。
这个func 依赖其他项目的 输出。
那么此时想要 进行 单元测试。屏蔽外部干扰。
如果不进行模拟而是进行,真正的依赖,那么很有可能导致错误的产生。
上游错误,导致下游所有测试失败。
多个错误发生了叠加,使得错误更难被追踪。
所以我们尽量采用 mock 来屏蔽外部的干扰,让程序得到预期的输入
在 Test_my_math 添加 模拟 测试函数指定返回 固定值
添加一个@patch 装饰器 来 模拟函数返回值
@patch("my_math.my_add", new=Mock(return_value=7)) def test_my_add_case_3(self): res = my_math.my_add(1, 5) self.assertEqual(7, res) python3 -m testtools.run test_my_math.py Ran 2 tests in 0.002s OK
指定 模拟函数 多次调用的 不同的返回值
from cinder import test from cinder.volume import ceph_api from cinder.tests.unit.volume.volume_constant import val import mock class TestCephApi(test.TestCase): def setUp(self): super(TestCephApi, self).setUp() self.api = ceph_api.API() self.context = None # return list when then input is right @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.RIGHT_POOL_INFO_OUTBUF, _], [_, val.RIGHT_POOL_DF_OUTBUF, _]])) def test_get_all_pools_with_right_input(self): res = self.api.get_all_pools(self.context) self.assertIsInstance(res, list) # return AttributeError when input is err @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.ERR_POOL_INFO_OUTBUF, _], [_, val.ERR_RIGHT_POOL_DF_OUTBUF, _]])) def test_get_all_pools_with_err_input(self): self.assertRaises(AttributeError, self.api.get_all_pools, self.context) # return dict when then input is right @mock.patch("cinder.volume.ceph_api.API.get_all_mons", mock.Mock(side_effect = [""])) @mock.patch("cinder.volume.ceph_api.API.get_all_pools", mock.Mock(side_effect = [""])) @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.RIGHT_POOL_STATUS_OUTBUF, _]])) def test_get_all_status_with_right_input(self): res = self.api.get_all_status(self.context) self.assertIsInstance(res, dict) # return keyerr when input is err @mock.patch("cinder.volume.ceph_api.API.get_all_mons", mock.Mock(side_effect = [""])) @mock.patch("cinder.volume.ceph_api.API.get_all_pools", mock.Mock(side_effect = [""])) @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.ERR_POOL_STATUS_OUTBUF, _]])) def test_get_all_status_with_err_input(self): self.assertRaises(KeyError, self.api.get_all_status, self.context)
side_effect 拓展用法,采用函数替换,patch 目标函数
如果希望更加动态的 指定 mock 返回值。
可以考虑采用一个 简单函数 来 mock 原有函数。
mock = Mock(return_value=3) def side_effect_func(*args, **kwargs): return DEFAULT mock.side_effect = side_effect_func mock()