前言
如果你写代码时会写单元测试(unit test,UT),那么多半会遇到想要将某个函数隔离开,去掉外部依赖的情况,例如这个函数依赖其他函数的返回或者依赖某个API调用的返回。这种情况下就一定绕不开mock这项技术。本文并不打算介绍python下mock的方方面面,只会写我个人实际使用中觉得比较实用的部分。
mocking是什么?
mock就是模拟的意思,mocking主要用在单元测试中,当被测试的对象依赖另一个复杂的对象时,我们需要模拟这个复杂的对象的行为,mocking就是创建这个对象,模拟它的行为。
基本用法
unittest.mock
模块包含了mock相关的功能。
Mock
对象
看如下一段代码:
from unittest.mock import Mock
# 创建一个mock对象
mock = Mock()
print(mock)
# 结果:<Mock id='140668914327224'> 表明此处的mock是一个Mock对象
Mock非常灵活,当我们访问一个Mock对象的某个属性时,这个属性如果不存在会被自动创建,看如下代码:
# 在访问之前mock并没有some_attribute这个属性
print(mock.some_attribute)
# 结果:<Mock name='mock.some_attribute' id='140348173848360'>
# 可见,在访问的时候创建了该属性
print(mock.do_something)
# 结果:<Mock name='mock.do_something' id='140348173886128'>
正是由于此特性,Mock可以用来模拟任意对象。
下面从最基本的开始介绍:
设置返回值和属性
Mock对象可以返回常量,也可以随着输入返回不同值。
返回常量-return_value
mock = Mock()
# 设置返回值
mock.return_value = 3
print(mock()) # 返回 3
# 设置方法的返回值
mock.method.return_value = 3
mock.method() # 返回 3
# 在构造函数中设置返回值
mock = Mock(return_value=3)
mock() # 返回 3
# 设置属性
mock = Mock()
mock.x = 3
mock.x # 返回 3
返回随着输入变化-side_effect
side_effect
也算一个属性,当你不满足指定一个常量返回时,就会期望用上它。
# 1.将side_effect设置为一个异常类
mock = Mock(side_effect=Exception('Boom!'))
mock() # 调用时就会抛出异常
from requests.exceptions import Timeout
requests = Mock()
requests.get.side_effect = Timeout # 模拟API超时
with self.assertRaises(Timeout):
# get_holidays函数里面调用了requests.get,那么将会捕获到Timeout异常
get_holidays()
# 将会抛出异常
# 2.将side_effect设置为一个迭代器(场景:mock对象被多次调用,每次返回值不一样)
mock = MagicMock(side_effect=[4, 5, 6])
mock()
4
mock()
5
mock()
6
# 3.将side_effect设置为一个函数(场景:返回值由输入参数决定)
vals = {(1, 2): 1, (2, 3): 2}
def side_effect(*args):
return vals[args]
mock = MagicMock(side_effect=side_effect)
mock(1, 2)
1
mock(2, 3)
2
mock一个类
def some_function():
instance = module.Foo()
return instance.method()
# 模拟Foo这个类
with patch('module.Foo') as mock:
# 此处的“mock”就是一个类,mock.return_value代表该类返回的实例(instance)
instance = mock.return_value
# 模拟实例方法的返回(此处的方法名就叫method)
instance.method.return_value = 'the result'
# 函数中对Foo的调用就会使用模拟类
result = some_function()
assert result == 'the result'
模拟一个对象(object)的方法(method)
patch
patch可能是使用最多的方法,它的使用场景:
1.模拟一个类的属性
2.模拟一个模块的属性
如果我们测试的函数在同一个文件中可以不使用patch,patch主要用在测试代码和主代码分离的情况下。
有3种装饰器可用:
# patch的第一个参数是一个string,形式:package.module.Class.attribute,以此指定要模拟的属性,第二个参数是可选的,第一个参数里面的属性将被替换为该值。
@patch('package.module.attribute', sentinel.attribute)
# 例子一,传有第二个参数:
mock = MagicMock(return_value=sentinel.file_handle)
with patch('builtins.open', mock):
handle = open('filename', 'r')
# 例子二,不传第二个参数:
# 不传第二个参数时,mock对象会被传入在函数的参数里,如下,并且注意顺序:
class MyTest(unittest.TestCase):
@patch('package.module.ClassName1')
@patch('package.module.ClassName2')
def test_something(self, MockClass2, MockClass1):
self.assertIs(package.module.ClassName1, MockClass1)
self.assertIs(package.module.ClassName2, MockClass2)
# 例子三,使用as,将会获得一个引用
with patch('ProductionClass.method') as mock_method:
mock_method.return_value = None
real = ProductionClass()
real.method(1, 2, 3)
# 例子四,将path装饰在类上,作用于每个测试函数
@patch('mymodule.SomeClass')
class MyTest(unittest.TestCase):
def test_one(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
def test_two(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
# 装饰在类上只针对test开头的函数,此处不是test开头,不传递MockSomeClass参数
def not_a_test(self):
return 'something'
# 例子五,另一种在整个类中模拟的办法
class MyTest(unittest.TestCase):
def setUp(self):
patcher = patch('mymodule.foo')
self.addCleanup(patcher.stop)
self.mock_foo = patcher.start()
def test_foo(self):
self.assertIs(mymodule.foo, self.mock_foo)
# patch的类在同一个文件中
# 此处需要使用__main__,代表当前模块
@patch('__main__.SomeClass')
# patch.object第一个参数是一个对象,第二个参数是该对象的属性名称,第三个是可选的,第二个参数里面的属性将被替换为该值。
# 场景:只模拟部分属性而非整个对象
@patch.object(SomeClass, 'attribute', sentinel.attribute)
@patch.dict()
如下为例子,供参考、copy:
# my_calendar.py
import requests
from datetime import datetime
def is_weekday():
today = datetime.today()
# Python's datetime library treats Monday as 0 and Sunday as 6
return (0 <= today.weekday() < 5)
def get_holidays():
r = requests.get('http://localhost/api/holidays')
if r.status_code == 200:
return r.json()
return None
# tests.py
import unittest
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch
class TestCalendar(unittest.TestCase):
# patch装饰在函数上如果函数里面会调用my_calendar下的requests函数,就会被mock掉
@patch('my_calendar.requests')
def test_get_holidays_timeout(self, mock_requests):
mock_requests.get.side_effect = Timeout
with self.assertRaises(Timeout):
get_holidays()
mock_requests.get.assert_called_once()
if __name__ == '__main__':
unittest.main()
mock可以直接使用装饰器,也可以使用上下文管理器,为什么使用上下文管理器?一般原因有如下两个,可自行判断要不要用:
1.只想针对部分代码,而不是整个测试函数
2.patch装饰器已经很多了,装饰器太多影响可读性
patch路径应该是什么?
where to patch?并不是要去引用某个函数本身所在的位置,而是要看这个函数在哪里使用的,如果在使用了的地方有import,那么就应该是那个地方的路径。
举例:
一个文件中(这个文件路径:package2.m2.py):
from package1.m1 import fun1
def fun2():
fun1()
在另一个测试文件中:
class JustTest(TestCase):
@patch('package2.m2.fun1') # 这才是正确的路径,而不是package1.m1.fun1
def test_fun2(self, mock_fun1):
mock_fun1.return_value = 3