Pulpcode

捕获,搅碎,拼接,吞咽

0%

初探python mock

Starting

假设你现在在写一个单元测试,你要测试的这个函数,调用了别的什么第三方接口,或者它要向某个数据库插入一条数据,或者是一个麻烦的资源,这个时候,你就需要用一个mock,来把这个“资源操作”替换。所以我们的mock实际上是为单元测试服务的。

存根还是模拟对象?

实际上如果你看过《单元测试的艺术》这本书,一定知道这样两个概念:模拟对象和存根。存根是指真实对象被替换之后,我更关心的是返回正确的值给我,比如模拟一个三方接口调用,而模拟对象更关心的是被调用对象本身的状态,比如向数据库插入一条数据,看数据库是否发生改变。实际上我们的mock对象,基本就是在做一个存根。当然后面的内容会让它在某些情况下看上去像一个模拟对象。

安装

在python2.x中,你只需要pip install mock就可以了,而3就不用了,因为3已经把mock当作标准库了,unittest.mock

第一个例子

在这个例子中,我想展示一下,如何来创建一个简单的mock,替换掉真实的函数调用。我们假设有一个接口,能通过传入的城市名和日期,返回当天的最低温度。

1
2
3
4
5
6
7
8
9
10
11
def fetch_low_weather(city, date):
url = "http://www.pulpcodeweather.com/%s/%s"
response = requests.get(url)
if response.code == 200:
return response.content
return None

def test_fetch_low_weather_4():
low_weather_mock = mock.Mock(return_value=4)
fetch_low_weather = low_weather_mock
assert fetch_low_weather("PEK", "2016-05-06") == 4

对于这个return_value而言,你可以在创建的时候对其进行设置>,或者是等它创建好了,在赋值,所以下面两种做法都是可以的。

1
mock.return_value = 5

第二个例子

在第二个例子中,我们可以通过传入不同的参数返回不同的结果,当你的return_value没有被设置,也就是None的时候,就会调用这个回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fetch_low_weather_side_effect(city, date):
if city == "PEK":
return 4
elif city == "SHA":
return 5
else:
return 0

def test_fetch_low_weather_side_effect_ok():
low_weather_mock = mock.Mock()
low_weather_mock.side_effect = fetch_low_weather_side_effect
fetch_low_weather = low_weather_mock
assert fetch_low_weather("PEK", "2016-04-05") == 4
assert fetch_low_weather("SHA", "2016-04-05") == 5

而且使用side_effect不仅可以传入参数并返回,还可以根据需要抛出异常,像这样:

1
mock.Mock(side_effect=KeyError("fuck"))

第三个例子

第三个例子中,我想讲一下,如何用patch功能进行深层次的替换,和查看状态,比如说你这里你要在数据库中插入一条数据,或者是删除一个文件,那么你要确定这个东西确实是被调用了,而且它真的不能有什么损伤,而且它能够被方便的替换。什么是指方便的替换?比如前面的例子,你可以直接“赋值”,让这个函数指针,引用到另一处。那么对于一些其它模块的系统调用呢?或者层级更深呢?

这个时候就要用路径引用法,实际上这也是一种编程的思路,我把这种东西成为“把动态变为静态”,你可以看看我之前的博客,有讲到logger对象的正确使用方式,也是想表达这种思路。

不仅仅是装饰器,mock还能够和with一起使用,实际上这个很好理解,patch确实是一种基于上下文的替换。

假设我们有一个python文件tools.py,里面的代码如下:

1
2
3
4
5
6
import os
import os.path

def rm(file):
if os.path.isfile(file):
os.remove(file)

那么我们在自己的代码中使用的它的时候,就可以这样替换。

1
2
3
4
5
6
@mock.patch("tools.os.path")
@mock.patch("tools.os")
def main(mock_os, mock_path):
mock_path.isfile.return_value = True
rm("fuck")
assert mock_os.remove.called == True

看,我们将os.path.isfile的返回值替换为了True,并且检验了os.remove函数是否被调用,而且你可以自己试一试,在目录中放一个文件,它并没有删除这个文件。所以即使是不检测这么复杂,仅仅想要不删除目录,那你只要patch就好了,需要注意的是patch装饰器的顺序和函数参数的属性是相反的,我想这是因为“参数压栈”的原因把。

第四个例子

最后一个例子,其实是我想在程序中使用mock的初衷,那就是我想要在tornado的异步调用中替换掉异步请求。首先你一定要使用tornado自带的测试框架,所以这个例子就是告诉你,在tornado中使用mock的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@tornado.gen.coroutine
def put_get_request_into_ioloop(context, url, origin=False):
"""
把一个异步get请求扔到tornado ioloop中
:param url: 请求url
:param handler_id: 处理id
:param origin: 是否日志请求和响应原串
:return:
True, 返回结果
False, None
"""
if origin:
http://logger.info("Request:%s\t%s" % (context["handler_id"], url))
try:
client = tornado.httpclient.AsyncHTTPClient()
response = yield client.fetch(url)
if origin:
http://logger.info("Response:%s\t%s" % (context["handler_id"], response.body))
except:
logger.error("ErrorRequest:%s\t%s" % (context["handler_id"], url), exc_info=True)
raise tornado.gen.Return((False, None))
else:
raise tornado.gen.Return((True, response.body))
1
2
3
4
5
6
7
8
9
10
11
class TestMock(tornado.testing.AsyncTestCase):
@mock.patch("tornado.httpclient.AsyncHTTPClient")
@tornado.testing.gen_test
def test_mock(self, AsyncHTTPClient):
AsyncHTTPClient.return_value = mock_http_client = mock.MagicMock()
fetch_future = tornado.concurrent.Future()
mock_http_client.fetch.return_value = fetch_future
fetch_future.set_result(mock.MagicMock(body="test"))
response = yield put_get_request_into_ioloop("the url")
self.assertEqual(response.body, "test")

更多的例子:

三篇不错的博客

https://segmentfault.com/a/1190000002965620

http://www.oschina.net/translate/unit-testing-with-the-python-mock-class

http://www.oschina.net/translate/an-introduction-to-mocking-in-python