Play Python Library之mock
Why use mock
mock
,库如其名,是一个用来伪造对象的库 (🙃)。
想象一个这样的场景:
你开发了一个比较复杂的程序,它每次都需要读取外部一个数据库中的数据,后续的操作都会根据这个读取的数据来进行。
面对这样的程序,你会如何写测试的代码?
是将依赖的数据库信息在测试代码中也定义一份?还是伪造一份数据库中的数据,然后hack掉读数据的部分,直接用伪造的数据用于后续操作的测试?
OK,以上两种方式都是可以达到测试的效果的。BUT,前者的测试代码会依赖外部的数据库,如果数据库服务器发生变化,那么测试代码也要跟着变;而且如果涉及数据库的写入,为了不影响开发环境的数据库,可能需要单独搭建一个测试用的数据库;即使搭建了测试用的数据库,如果开发用的数据库配置发生变化,测试这边也得跟着变,想想就好麻烦。而后者使用Hack的方式来取代掉外部的数据库虽然可行,但测试代码会变得难以维护。
解决这个问题还有一个出路,那就是用mock
库中的MagicMock
对象来模拟和伪造一个数据库对象,它帮你干了你要干的hack的活,用这种方式要比直接hack源代码要更易于维护。
Play with mock
MagicMock
简单提几点:
- 访问
MagicMock
类型的对象中未定义的属性会创建一个新MagicMock
并绑定到这个属性。 MagicMock
类型有一些特殊的属性:- 通过设定
MagicMock
类型对象的return_value
属性,会把这个属性的值绑定为__call__
方法的返回值。 - 通过设定
MagicMock
类型对象的side_effect
属性,可以达到使__call__
方法根据输入来返回不同值的效果。
- 通过设定
其他关于MagicMock
的一些特性看下面的例子好了:
from mock import MagicMock
if __name__ == '__main__':
# MagicMock object will generate a new MagicMock object if you visit to a undefined attribute.
m0 = MagicMock()
print m0.aaa
# You can define some attributes in the construct function of MagicMock conveniently.
m1 = MagicMock(aaa=1, bbb=2)
print m1.aaa, m1.bbb
# By `del` method, you can delete an attribute of MagicMock object, and it will cause AttributeError when you visit the deleted attribute.
del m1.ccc
try:
print m1.ccc
except AttributeError, e:
print 'AttributeError'
# Use spec to limit a set of attributes, will cause AttributeError when you visit attribute out of the spec list.
m2 = MagicMock(spec=['aaa', 'bbb'])
print m2.aaa
try:
print m2.ccc
except AttributeError, e:
print 'AttributeError'
# Use spec to input a object can make the MagicMock object pass `isinstance` test.
dd = dict()
m3 = MagicMock(spec=dd)
print isinstance(m3, dict)
# Set the `return_value` to a MagicMock object will let the calling of the object always return the same value.
m4 = MagicMock()
m4.return_value = 6
print m4(4, 'aaa'), m4([1,2,3])
# Use side_effect to bind a method to the MagicMock object so that it can return different values according to the inputs.
m4 = MagicMock(side_effect=lambda value: value * 2)
print m4(3), m4('abc')
# Can also bind a iterable object to side_effect. In this case, the return value will be the traversal of the iterable object.
# And has nothing to do with the inputs.
m5 = MagicMock()
m5.side_effect = [5, 'hi']
print m5(2), m5()
patch
通过patch
可以方便地替换掉目标位置的对象,从而达到mock的目的。mock
很贴心地帮patch
写了装饰器和with语句的实现,让替换对象的scope更加一目了然。
使用patch
要特别注意要替代的对象定位不能错(参见Mock patching for ‘from/import’ statement in Python),其他直接看下面的例子就行:
from mock import patch
def foo():
return 'foo'
# patch can simply used by only inputting the target object that need to replaced.
# In this situation, you must append an argument to the method's input. This argument is a MagicMock object generated by patch decorator.
# So you can change the return value of the MagicMock object which will be the return value of calling the replaced object.
@patch('__main__.foo')
def test_one(mock_fuc):
mock_fuc.return_value = 'bar'
print foo
print foo()
# One goodness of inputting MagicMock object into method is here: you can make some assert of the mock object.
mock_fuc.assert_called_once_with()
print mock_fuc.call_count
def bar():
return 'bar'
# patch can also used by inputting the replaced object in the decorator.
# In this situation, you do not need to append an argument to the method's input.
@patch('__main__.foo', bar)
def test_two():
print foo
print foo()
# Besides decorator, you can also use patch as `with` statement.
def test_three():
with patch('__main__.foo', bar):
print foo
print foo()
print foo
print foo()
# Besides `with` statement, you can also control the scope by patch.start() and patch.stop() methods.
# This is more flexible than the methods above, and can be useful when patch with several test cases.
def test_four():
patcher = patch('__main__.foo', bar)
patcher.start()
print foo
print foo()
patcher.stop()
print foo
print foo()
# Can use several patch at the same time.
# When inputting the MagicMock objects to the method, notice the sequence of the MagicMock objects is reverse to the decorators.
@patch('__main__.foo')
@patch('__main__.bar')
def test_five(mock_bar, mock_foo):
mock_foo.return_value = 'zzz'
mock_bar.return_value = 'mew'
print foo
print foo()
print bar
print bar()
if __name__ == '__main__':
test_one()
test_two()
test_three()
test_four()
test_five()
patch.object, patch.dict, patch.multiply
这三个东西其实就是让你少些了几行代码,当然用还是蛮好用的:
from mock import patch, DEFAULT
class Real(object):
def foo(self):
return 'foo'
def bar(self):
return 'bar'
# Use `patch.object` to replace only one attribute of the object.
# (This function can be also implemented by `patch`, but this is more convenient)
@patch.object(Real, 'foo')
def test_one(mock_foo):
mock_foo.return_value = 'zzz'
foo = Real()
print foo.foo()
print foo.bar()
real_dict = {'foo': 1, 'bar': 2, 'zzz': 3}
# Use `patch.dict` to 'revise' a dict like object conveniently.
@patch.dict(real_dict, foo=2, bar=3)
def test_two():
print real_dict
# Use `patch.multiple` to replace several objects in the same module.
@patch.multiple('__main__', real_dict={}, Real=DEFAULT)
def test_three(Real):
print real_dict
print Real
if __name__ == '__main__':
test_one()
test_two()
test_three()
patch.TEST_PREFIX
因为mock
这个库本身是为测试而写出来的,在Python 3中也是作为unittest库的一部分存在的,所以它默认只会替换测试方法中的对象,而ignore非测试方法。现在主流的测试框架对测试方法的判断就是看它是不是以test
为开头命名的,所以mock
也是这么判断的。
如果用mock
不是用于测试,可以修改patch.TEST_PREFIX
的值来让它生效:
from mock import patch
# By default, the patchers recognise methods that start with 'test' as being test methods.
# We set the prefix to `foo` here so that the patch will influence all the methods whose name start with `foo`
patch.TEST_PREFIX = 'foo'
value = 3
@patch('__main__.value', 'not three')
class Thing(object):
def foo_one(self):
print value
def foo_two(self):
print value
def bar_one(self):
print value
if __name__ == '__main__':
t = Thing()
t.foo_one()
t.foo_two()
t.bar_one()
An example
最后看下官方文档的一个例子,我添加了一些注释:
from mock import patch
class Foo(object):
def print_foo(self):
return 'foo'
def some_function():
instance = Foo()
print instance
return instance.print_foo()
if __name__ == '__main__':
# Will create a MagicMock object to replace class `Foo`
with patch("__main__.Foo") as MockFoo:
# Call the MagicMock object will return a new MagicMock object and is bounded with the calling MockFoo()
# (means each time you call MockFoo()/Foo(), will return the same MagicMock object)
mock_instance = MockFoo.return_value
# Set the print_foo()'s return value to 'bar'
mock_instance.print_foo.return_value = 'bar'
print mock_instance
# Call the function which calls Foo.print_foo(), this actually calls the mock_instance.print_foo()
result = some_function()
assert result == 'bar'
Comments