python mock初步
从Android单元测试中,接触到Java的mock框架。参看这篇笔记《Android单元测试》。最近在做Dashboard Generator,要用python解析excel,ppt和网页,测试效率极低。所以考虑到可以用mock来代替一些耗时的操作和资源。于是接触到python的mock框架。
python mock在3.0中已经集成为内置类,但是2.7仍然需要用pip来安装。这也不难。然后在网上搜集了几篇还不错的文章,如下:
简介
因为是python语言的特性,所以Mock上手非常容易。窃以为比Java(Android) Mock要容易的多。
Mock vs. Stub/Fake
参看Using Mocks in Python,也可以参看Martin Fowler的一篇文章Mocks Aren’t Stubs。
这里提到了Mock和Stub/Fake的区别。这是我以前一直没有注意到的。
为什么要用Mock?
A good unit test has to be repeatable, allowing you to isolate and identify a fault. But the test resource might give out stochastic results, or it might have widely varying response times. And as a result, you end up glossing over a potential showstopper.
测试必须是可重复的,解耦的。但代码用到的运行时资源往往是随机的,或者昂贵的(比如网页,总是在变化的,比如,download文件,非常耗时)。我们不能让我们的测试首先不能依赖于测试资源,其次需要有比较高的效率。
These are some of the reasons why you might want to use a mock in place of a test resource. A mock can present the same set of method interfaces as the resource to the test subject. But a mock is easier to setup, easier to manage. It can deliver deterministic results, and it can be customized to suit a particular test. And it can be easily updated to reflect changes in the actual resource.
Stubs & Fake
Stub
A stub presents a set of method interfaces to the test subject, the same set the subject would see in an actual test resource. When the test subject calls a stub method, the stub may respond with a predetermined set of results. It may raise an error or exception, also predetermined. A stub may track its interactions with the test subject, but it performs no tasks outside the narrow scope of its programming.
Fake
A fake also presents a set of method interfaces and tracks its interactions with the test subject. But unlike a stub, a fake actually processes input data from the test subject and produces results based on that data. In short, a fake is a functional, but non-production version of the actual test resource. It lacks the checks and balances found in resource, it uses simpler algorithms, and it seldom, if ever, stores or transports data.
Stub和Fake都是针对接口的模拟,在需要对状态进行测试的情况时,我们可以用Stub或者Fake。例如UnitTest++,就是用的Stub。
但是如果我们要测试具体的代码行为,我们往往要用到Mock。比如,一个函数调用了几次,用的什么参数,等等。
我的理解,其实他们三者之间并没有很明显的界限。用Mock的时候,针对一些需要模拟的接口,仍然可以有办法将之stub out。只是Mock并不擅长,方法不够直觉。同理Stub也可以用来mock掉一些对象。
Stub/Fake更像是应用于面向接口编程,而Mock更擅长于面向对象编程。
何时使用Mock
Of course, mocks are not without issues. Designing an accurate mock is difficult, especially if you have no reliable information on the test resource. You can try and find an open-source match, or you can make a guesstimate on the resource’s method interface. Whichever you choose, you can update the mock easily later on, should you get more detailed information on the preferred resource. Too many mocks can complicate a test, making it harder for you to track down a failure.
Best practice
- is to limit a test case to one or two mocks
- or use separate test cases for each mock/subject pairing.
如何使用
constructor
class Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, **kwargs)
主要参数如下:
- name: 只有标记作用,在print或者repr的时候可以看到,调试的时候有用
- return_value: 最简单的对接口的stub化
- side_effect: 其参数可以为列表或函数,若其返回值为DEFAULT,则采用return_value的值,若其返回值不是DEFAULT,则其会覆盖return_value的值
- spec: 可以是string列表,也可以为class或instance。Accessing any attribute not in this list will raise an AttributeError.
side_effect为列表
>>> mock = Mock()
>>> mock.side_effect = [3, 2, 1]
>>> mock(), mock(), mock()
(3, 2, 1)
side_effect为函数,其参数与真实传入的参数相同
>>> side_effect = lambda value: value + 1
>>> mock = Mock(side_effect=side_effect)
>>> mock(3)
4
>>> mock(-8)
-7
对函数调用的检测
mock包含以下属性:
- called
- call_count
- call_args
- call_args_list
含义显而易见,用法参考官方文档
method_calls & mock_calls
>>> mock = Mock()
>>> mock.method()
<Mock name='mock.method()' id='...'>
>>> mock.property.method.attribute()
<Mock name='mock.property.method.attribute()' id='...'>
>>> mock.method_calls
[call.method(), call.property.method.attribute()]
>>> mock = MagicMock()
>>> result = mock(1, 2, 3)
>>> mock.first(a=3)
<MagicMock name='mock.first()' id='...'>
>>> mock.second()
<MagicMock name='mock.second()' id='...'>
>>> int(mock)
1
>>> result(1)
<MagicMock name='mock()()' id='...'>
>>> expected = [call(1, 2, 3), call.first(a=3), call.second(),
... call.__int__(), call()(1)]
>>> mock.mock_calls == expected
True
assert
Mock提供了以下几种assert:
- assert_called_once
- assert_called_with
- assert_called_once_with
- assert_has_calls
- assert_any_calls
前面三个很直觉,后面两个稍作解释。
assert_any_call
The assert assert_any_call() checks if the test subject called a mocked method at any point of the test routine. This is regardless of how many other calls were made between the mocked method and the assert.
assert_has_calls
This one looks at a sequence of method calls, checks if they are in the right order and with the right arguments.
from mock import Mock, call
# The mock specification
class Foo(object):
_fooValue = 123
def callFoo(self):
pass
def doFoo(self, argValue):
pass
# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns <Mock spec='Foo' id='507120'>
mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")
fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls)
# AssertionError: Calls not found.
# Expected: [call.callFoo(), call.dooFoo('narf'), call.doFoo('zort')]
# Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]
fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls, any_order = True)
# AssertionError: (call.dooFoo('narf'),) not all found in call list
高阶特性
MagicMock vs. Mock
With Mock you can mock magic methods but you have to define them. MagicMock has “default implementations of most of the magic methods“.
If you don’t need to test any magic methods, Mock is adequate and doesn’t bring a lot of extraneous things into your tests. If you need to test a lot of magic methods MagicMock will save you some time.
哪些magic methods?看下面
- lt: NotImplemented
- gt: NotImplemented
- le: NotImplemented
- ge: NotImplemented
- int : 1
- contains : False
- len : 1
- iter : iter([])
- exit : False
- complex : 1j
- float : 1.0
- bool : True
- nonzero : True
- oct : ‘1’
- hex : ‘0x1’
- long : long(1)
- index : 1
- hash : default hash for the mock
- str : default str for the mock
- unicode : default unicode for the mock
- sizeof: default sizeof for the mock
>>> mock = MagicMock()
>>> int(mock)
1
>>> len(mock)
0
>>> hex(mock)
'0x1'
>>> list(mock)
[]
>>> object() in mock
False
所谓magic methods就是一些内置的python运算符,而MagicMock支持这些运算符,Mock不支持
mock import
Mocking Python imports这里有提到。其实很简单,看代码
# A.py
import B
B.foo()
# test.py
A.B = mock.Mock()
A.B.foo.return_value = 123
mock class constructor
直接贴上实际的测试代码
mockSMBConnObj = mock.Mock()
mockSMBConn = mock.Mock(return_value=mockSMBConnObj) # mock constructor
mock对象的自动创建
当访问一个mock对象中不存在的属性时,mock会自动建立一个子mock对象,并且把正在访问的属性指向它,这个功能对于实现多级属性的mock很方便。
client = mock.Mock()
client.v2_client.get.return_value = '200'
上面例子中client.v2_client.get
就是自动生成的mock对象
类似自动生成,我们还可以采用attach_mock
的方法。不同的是,attach_mock会让被attach的双方具有父子关系,则对子mock的调用会被计入父的mock_calls。参考Attaching Mocks as Attributes
patch
通过patch,我们可以控制Mock对象的生存周期。意思就是在一个函数范围内,或者一个类的范围内,或者with语句的范围内mock掉一个对象。
有下面
decorator
>>> @patch('__main__.SomeClass')
... def function(normal_argument, mock_class):
... print mock_class is SomeClass
...
>>> function(None)
True
context manager
>>> class Class(object):
... def method(self):
... pass
...
>>> with patch('__main__.Class') as MockClass:
... instance = MockClass.return_value
... instance.method.return_value = 'foo'
... assert Class() is instance
... assert Class().method() == 'foo'
patcher
>>> Original = Class
>>> patcher = patch('__main__.Class', spec=True)
>>> MockClass = patcher.start()
>>> instance = MockClass()
>>> assert isinstance(instance, Original)
>>> patcher.stop()
patch.object和patch类似,只是写法不同,看下面对比:
@patch.object(SomeClass, 'class_method')
@patch('SomeClass.class_method')
patch还有很多种其他用法,patch.multiple, patch.dict等等。参看More Patch
其他未决的问题
其实还有很多mock高级的用法,例如,当我想mock掉win32com的时候,搜到的这个网页Mocking out python OS specific imports。其中有一段代码:
modules = {
"_winreg": mock.MagicMock(),
"pythoncom": mock.MagicMock(),
"pywintypes": mock.MagicMock(),
"win32api": mock.MagicMock(),
"win32com": self.win32com,
"win32com.client": self.win32com.client,
"win32file": mock.MagicMock(),
"win32service": mock.MagicMock(),
"win32serviceutil": mock.MagicMock(),
"winerror": mock.MagicMock(),
"wmi": self.wmimock
}
self.module_patcher = mock.patch.dict("sys.modules", modules)
self.module_patcher.start()
还有如何mock掉win32com.client的对象?例如,待测代码如下
Application = win32com.client.Dispatch("PowerPoint.Application")
source_ppt = Application.Presentations.Open(path)
source_ppt.Slides.InsertFromFile(os.path.abspath(chart), index, 1, 1)
因为对win32com.client
实现一无所知,不知道如何mock,推测mock代码如下,还未验证
mockApp = mock.Mock()
with mock.patch('MyModule.win32com.client.Dispatch', return_value=mockApp) as mockWin32com:
mockPpt = mock.Mock()
mockApp.Presnetations.Open.return_value = mockPpt
mockPpt.Slides.InsertFromFile.assert_called_once_with(...)
因为python的语言特性,python的Mock框架还是比Java的Mock框架要灵活很多。比如:对类构造函数的Mock,非常容易,而Java则需要经过特殊处理。还有对接口的fake化,用side_effect很容易实现,但是Java Mock则要用到when().thenAnswer(){}
。麻烦又不直觉。
when(...).thenAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return value;
}
});