Python写单元大多数都会用到unittest和mock,测试代码覆盖率都会用到coverage
Unittest
unittest.mock 官网 https://docs.python.org/3/library/unittest.mock.html
unittest就不详细介绍了,注意几点:
- 测试类继承unittest.TestCase
- 测试类、测试方法名字最好以test开头,很多工具能根据名字来自动运行,很方便
- 测试类里面的setUp/tearDown会在每个case执行之前/之后执行,setUpClass/tearDownClass加上@classmethod在整个测试类开始和结束的时候执行
- 测试文件的main函数里面加上unittest.main(),就可以直接用python命令运行了
Mock
单元测试里面比较精髓的就是mock了,介绍几种常见的场景:
1. Mock一个函数 patch
其实有好几种方法,个人比较推荐下面这种,看上去很清晰:
mock.patch
def multiple(a, b):
return a*b
class TestProducer(unittest.TestCase):
def setUp(self):
self.calculator = Calculator()
@mock.patch('multiple')
def test_multiple(self, mock_multiple):
mock_multiple.return_value = 3
self.assertEqual(multiple(8, 14), 3)
mock.patch 返回一个对象, 可以通过 start/stop 方法来应用
class DemoTestCase(unittest.TestCase):
def setUp(self):
super(DemoTestCase, self).setUp()
self._patcher = mock.patch("pkg.mod.dep_mod.func", autospec=True)
self._func_mock = self._patcher.start()
self._func_mock.return_value = 1
def tearDown(self):
super(DemoTestCase, self).tearDown()
# cleanup
self._patcher.stop()
def test_demo(self):
# real test code
self._func_mock.assert_called_with(arg1, arg2)
使用装饰器和上下文管理器
装饰器:
# -*- coding: utf-8 -*-
from unittest.mock import patch
from strategy.module import ProductionClass
@patch('strategy.module.ProductionClass.method', return_value=3)
def test(method):
print(ProductionClass().method(3, 4, key='value'))
method.assert_called_with(3, 4, key='value')
test()
上下文管理器:
# -*- coding: utf-8 -*-
from unittest.mock import patch
from strategy.module import ProductionClass
def test():
with patch.object(ProductionClass, 'method', return_value=3) as mock_method:
print(ProductionClass().method(3, 4, key='value'))
mock_method.assert_called_with(3, 4, key='value')
test()
对于其他对象,比如函数,只需要将 patch
中的路径变为函数的路径即可。
2. Mock一个对象里面的方法 object
mock.patch.object
class Calculator(object):
def add(self, a, b):
return a+b
class TestProducer(unittest.TestCase):
def setUp(self):
self.calculator = Calculator()
@mock.patch.object(Calculator, 'add')
def test_add(self, mock_add):
mock_add.return_value = 3
self.assertEqual(self.calculator.add(8, 14), 3)
3. Mock的函数每次被调用返回不同的值 side_effect
让Mock的函数每次被调用返回不同的值,而1,2中的方法每次调用都会返回同样的值
class TestProducer(unittest.TestCase):
@mock.patch.object(Calculator, 'add')
def test_effect(self, mock_add):
mock_add.side_effect = [1, 2, 3]
self.assertEqual(self.calculator.add(8, 14), 1)
self.assertEqual(self.calculator.add(8, 14), 2)
self.assertEqual(self.calculator.add(8, 14), 3)
4. 让Mock的函数抛出exception
def is_error(self):
try:
os.mkdir("11")
return False
except Exception as e:
return True
class TestProducer(unittest.TestCase):
@mock.patch('os.mkdir')
def test_exception(self, mock_mkdir):
mock_mkdir.side_effect = Exception
self.assertEqual(self.calculator.is_error(), True)
side_effect 可以是一个函数、iterable 或 exception(类或实例)。
关于更多详情见:
https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.side_effect
5. Mock多个函数,主要是注意顺序
参数由下到上
@mock.patch.object(Calculator, 'add')
@mock.patch('test_unit.multiple')
def test_both(self, mock_multiple, mock_add):
mock_add.return_value = 1
mock_multiple.return_value = 2
self.assertEqual(self.calculator.add(8, 14), 1)
self.assertEqual(multiple(8, 14), 2)
6. Mock contextlib 上下文管理
如果当前单元测试仅有部分测试依赖该组件也可以通过上下文管理的方式进行管理, 更加灵活.
import contextlib
class DemoTestCase(unittest.TestCase):
@contextlib.contextmanger
def _mock_context(self):
patcher = mock.patch("pkg.mod.dep_mod.func", autospec=True)
try:
func_mock = patcher.start()
func_mock.return_value = 1
yield func_mock
finally:
patcher.stop()
def test_demo(self):
with self._mock_context() as func_mock:
# real test code
func_mock.assert_called_with(arg1, arg2)
7. Mock和MagicMock的区别
详解:
https://segmentfault.com/a/1190000008742467
https://blog.csdn.net/qq_16077957/article/details/80023068
8. pytest fixture
我们写单元测试时,有些涉及到复杂的功能、数据库等待session范围,需要使用到一些高级特性了
详解:
https://www.cnblogs.com/huizaia/p/10331469.html
https://www.cnblogs.com/linuxchao/p/linuxchao-pytest-fixture.html
9. pytest 多组参数 parametrize
@pytest.mark.parametrize(“参数名”,列表数据)
参数名:用来接收每一项数据,并作为测试用例的参数。
列表数据:一组测试数据。
@pytest.mark.parametrize("a, b, c", [(1, 2, 3), (4, 5, 9)])
def test_add01(a, b, c):
res = a + b
assert res == c
Coverage
打命令coverage加测试文件,就可以得到覆盖率,可以生成html格式的报告,每次运行一个文件都会生成一个.coverage文件,需要将combine所有结果才能得到一个完整的报告。
具体的命令参数参看:http://nedbatchelder.com/code/coverage/cmd.html
更加有用的是配置文件,参看:http://nedbatchelder.com/code/coverage/config.html