在Python单元测试中更好的模拟类属性的方法
我有一个基础类,这个类定义了一个类属性,还有一些依赖于这个属性的子类,比如:
class Base(object):
assignment = dict(a=1, b=2, c=3)
我想用不同的“赋值”来对这个类进行单元测试,比如空字典、单个项目等等。当然,这只是一个极简化的例子,实际上并不是要重构我的类或测试。
我最终想出来的(pytest)测试代码是:
from .base import Base
def test_empty(self):
with mock.patch("base.Base.assignment") as a:
a.__get__ = mock.Mock(return_value={})
assert len(Base().assignment.values()) == 0
def test_single(self):
with mock.patch("base.Base.assignment") as a:
a.__get__ = mock.Mock(return_value={'a':1})
assert len(Base().assignment.values()) == 1
这感觉有点复杂和不太靠谱——我甚至不完全理解为什么它能工作(不过我对描述符有一些了解)。难道mock会自动把类属性变成描述符吗?
一个感觉更合理的解决方案却不管用:
def test_single(self):
with mock.patch("base.Base") as a:
a.assignment = mock.PropertyMock(return_value={'a':1})
assert len(Base().assignment.values()) == 1
或者直接这样:
def test_single(self):
with mock.patch("base.Base") as a:
a.assignment = {'a':1}
assert len(Base().assignment.values()) == 1
我尝试的其他变体也不行(测试中的赋值没有改变)。
那么,正确的方式来模拟一个类属性是什么呢?有没有比上面的方法更好、更容易理解的方式呢?
6 个回答
7
这里有一个示例,教你如何对你的 Base
类进行单元测试:
- 模拟多个不同类型的类属性(比如:
dict
和int
) - 使用
@patch
装饰器和pytest
框架,适用于python 2.7+
或3+
。
# -*- coding: utf-8 -*-
try: #python 3
from unittest.mock import patch, PropertyMock
except ImportError as e: #python 2
from mock import patch, PropertyMock
from base import Base
@patch('base.Base.assign_dict', new_callable=PropertyMock, return_value=dict(a=1, b=2, c=3))
@patch('base.Base.assign_int', new_callable=PropertyMock, return_value=9765)
def test_type(mock_dict, mock_int):
"""Test if mocked class attributes have correct types"""
assert isinstance(Base().assign_dict, dict)
assert isinstance(Base().assign_int , int)
7
如果你的类(比如队列 Queue)已经在测试中被引入了,而你想要修改MAX_RETRY这个属性,你可以使用@patch.object,或者更简单的方式是使用@patch.multiple。
from mock import patch, PropertyMock, Mock
from somewhere import Queue
@patch.multiple(Queue, MAX_RETRY=1, some_class_method=Mock)
def test_something(self):
do_something()
@patch.object(Queue, 'MAX_RETRY', return_value=1, new_callable=PropertyMock)
def test_something(self, _mocked):
do_something()
12
为了让代码更容易读懂,你可以使用 @patch
这个装饰器:
from mock import patch
from unittest import TestCase
from base import Base
class MyTest(TestCase):
@patch('base.Base.assignment')
def test_empty(self, mock_assignment):
# The `mock_assignment` is a MagicMock instance,
# you can do whatever you want to it.
mock_assignment.__get__.return_value = {}
self.assertEqual(len(Base().assignment.values()), 0)
# ... and so on
你可以在这里找到更多详细信息:http://www.voidspace.org.uk/python/mock/patch.html#mock.patch。
19
我可能理解错了,但是不是可以不使用 PropertyMock
就实现这个功能呢?
with mock.patch.object(Base, 'assignment', {'bucket': 'head'}):
# do stuff
55
base.Base.assignment
只是被一个 Mock
对象替代了。你通过添加一个 __get__
方法让它变成了一个描述符。
这样写有点啰嗦,也有点多余;你其实可以直接设置 base.Base.assignment
:
def test_empty(self):
Base.assignment = {}
assert len(Base().assignment.values()) == 0
当然,这在使用测试并发时不是特别安全。
如果要使用 PropertyMock
,我会这样做:
with patch('base.Base.assignment', new_callable=PropertyMock) as a:
a.return_value = {'a': 1}
甚至可以这样:
with patch('base.Base.assignment', new_callable=PropertyMock,
return_value={'a': 1}):