本文共 17817 字,大约阅读时间需要 59 分钟。
Django项目test中的mock概述
本文环境python3.5.2
test中的mock实现
接口示例代码如下;
... # 路由配置 ('^api/business_application/?$', TestAPI.as_view()), ... # 接口函数 app_name.apis.pyfrom rest_framework.generics import GenericAPIViewfrom rest_framework.response import Responsedef mock_test(test_val): print("test_mock_val") return "real_return" + test_val class TestAPI(GenericAPIView): view_name = 'test' def get(self, request): data = mock_test("real") return Response({"detail": data})
test测试代码如下:
from unittest.mock import patchfrom django.test import TestCaseclass MockAPITest(TestCase):"""mock测试""" def setUp(self): self.url = reverse('app_name:test') @patch('app_name.apis.mock_test') def test_post(self, mock_test): mock_test.return_value = "mock" resp = self.client.get(self.url) retdata = resp.data self.assertEqual(retdata['detail'], "mock")
此时在终端中,运行该用例测试结果正常,此时分析一下在该示例中mock的基本用法,由于本次测试的场景相对简单故在源码分析中,复杂的场景就不详细分析。
unittest.mock中的patch分析
首先查看patch函数的执行流程如下;
def _dot_lookup(thing, comp, import_path): try: return getattr(thing, comp) # 获取导入的属性 except AttributeError: __import__(import_path) return getattr(thing, comp)def _importer(target): components = target.split('.') # 此时传入的target为app_name.apis import_path = components.pop(0) # 获取app_name thing = __import__(import_path) # 导入app_name for comp in components: import_path += ".%s" % comp # 遍历路径名称 thing = _dot_lookup(thing, comp, import_path) # 依次获取导入的属性名称 return thing # 遍历完成后此时就获取到了 mock_test对应的方法属性def _get_target(target): try: target, attribute = target.rsplit('.', 1) # 获取module路径 和方法名称,app_name.apis 和 mock_test方法 except (TypeError, ValueError): raise TypeError("Need a valid target to patch. You supplied: %r" % (target,)) getter = lambda: _importer(target) # 封装成函数等待调用 return getter, attribute # 返回def patch( target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs ): getter, attribute = _get_target(target) # 获取传入的属性对应的值 return _patch( getter, attribute, new, spec, create, spec_set, autospec, new_callable, kwargs )
此时先去导入相关的module,去获取对应mock函数的函数,然后再传入_patch类中进行初始化,
class _patch(object): attribute_name = None _active_patches = [] def __init__( self, getter, attribute, new, spec, create, spec_set, autospec, new_callable, kwargs ): if new_callable is not None: if new is not DEFAULT: raise ValueError( "Cannot use 'new' and 'new_callable' together" ) if autospec is not None: raise ValueError( "Cannot use 'autospec' and 'new_callable' together" ) self.getter = getter # 属性获取对应 self.attribute = attribute # 属性方法 self.new = new self.new_callable = new_callable self.spec = spec self.create = create self.has_local = False self.spec_set = spec_set self.autospec = autospec self.kwargs = kwargs self.additional_patchers = []def copy(self): patcher = _patch( self.getter, self.attribute, self.new, self.spec, self.create, self.spec_set, self.autospec, self.new_callable, self.kwargs ) patcher.attribute_name = self.attribute_name patcher.additional_patchers = [ p.copy() for p in self.additional_patchers ] return patcherdef __call__(self, func): if isinstance(func, type): # 在测试中调用的该方法 return self.decorate_class(func) return self.decorate_callable(func)
由于先前博文已经分析过test的执行过程,此时test会执行到test(result)函数方法,由于本例中的test_post由装饰器patch修饰,此时返回的是_patch类的实例,当调用test_post方法时,就会调用__call__方法,由于此时test_post是一个call_func所以会执行到self.decorate_callable函数处,该函数如下;
def decorate_callable(self, func): if hasattr(func, 'patchings'): # 检查func是否有patching属性,该属性为一个列表,该属性用于存放_patch对象,处理一个方法经过了多个_patch对象装饰 func.patchings.append(self) # 如果有则添加到该属性列表中 return func # 返回该函数 @wraps(func) def patched(*args, **keywargs): # 当test真正执行时,调用该方法 extra_args = [] # 传入参数 entered_patchers = [] exc_info = tuple() # 报错信息 try: for patching in patched.patchings: # 遍历patchings列表 arg = patching.__enter__() # 调用_patch的__enter__方法 entered_patchers.append(patching) # 添加到entered_patchers列表中 if patching.attribute_name is not None: # 检查_patch对应的需要mock的方法名是否为空 keywargs.update(arg) # 不为空则更新传入参数 elif patching.new is DEFAULT: extra_args.append(arg) # 检查传入的是否是默认参数,如果没有使用位置参数则使用位置参数 args += tuple(extra_args) # 增加到位置参数中 return func(*args, **keywargs) # 调用该test对应的方法,本例中为test_post方法 except: if (patching not in entered_patchers and _is_started(patching)): # 报错处理 # the patcher may have been started, but an exception # raised whilst entering one of its additional_patchers entered_patchers.append(patching) # Pass the exception to __exit__ exc_info = sys.exc_info() # re-raise the exception raise finally: for patching in reversed(entered_patchers): # 执行正确最后执行_patch对应的__exit__方法 patching.__exit__(*exc_info) patched.patchings = [self] # 添加该属性 return patched # 返回该patched方法
该函数主要就是执行了包装后的_patch方法,依次执行_patch的__enter__方法,当执行完成后,依次调用_patch的__exit__的方法,此时就依次完成对测试用例的执行,继续查看_patch对应的__enter__方法;
def get_original(self): target = self.getter() # 获取命名空间的module name = self.attribute # 获取需要Mock的函数名 original = DEFAULT # 默认为DEFAULT local = False # 是否本地空间中找到 默认为False try: original = target.__dict__[name] # 获取需要mock的函数,在本例中因为在同一命名空间中,是可以获取到该函数 except (AttributeError, KeyError): original = getattr(target, name, DEFAULT) else: local = True # 修改标志位 if name in _builtins and isinstance(target, ModuleType): # 判断是否为在内建名称中,判断target是否为module self.create = True if not self.create and original is DEFAULT: raise AttributeError( "%s does not have the attribute %r" % (target, name) ) return original, local # 返回原始函数,是否在本地命名空间找到标志def __enter__(self): """Perform the patch.""" new, spec, spec_set = self.new, self.spec, self.spec_set autospec, kwargs = self.autospec, self.kwargs new_callable = self.new_callable # 获取初始化时传入的new_callable方法 self.target = self.getter() # 获取对应的moduel # normalise False to None # 一般情况下设置为空或者False if spec is False: spec = None if spec_set is False: spec_set = None if autospec is False: autospec = None if spec is not None and autospec is not None: raise TypeError("Can't specify spec and autospec") if ((spec is not None or autospec is not None) and spec_set not in (True, None)): raise TypeError("Can't provide explicit spec_set *and* spec or autospec") original, local = self.get_original() # 获取被mock函数的原始函数,是否在module的命名空间中找到 if new is DEFAULT and autospec is None: # 本例中new为DEFAULT autospec为None inherit = False # 是否继承 if spec is True: # set spec to the object we are replacing spec = original if spec_set is True: spec_set = original spec = None elif spec is not None: if spec_set is True: spec_set = spec spec = None elif spec_set is True: spec_set = original if spec is not None or spec_set is not None: if original is DEFAULT: raise TypeError("Can't use 'spec' with create=True") if isinstance(original, type): # If we're patching out a class and there is a spec inherit = True Klass = MagicMock # 设置MagicMock类,该类就是替代mock的函数的类 _kwargs = {} # 设置类的传入的位置参数 if new_callable is not None: # 如果在传入的时候指定了new_callable函数 Klass = new_callable # 使用传入的类作为mock的类实例 elif spec is not None or spec_set is not None: this_spec = spec if spec_set is not None: this_spec = spec_set if _is_list(this_spec): not_callable = '__call__' not in this_spec else: not_callable = not callable(this_spec) if not_callable: Klass = NonCallableMagicMock if spec is not None: _kwargs['spec'] = spec if spec_set is not None: _kwargs['spec_set'] = spec_set # add a name to mocks if (isinstance(Klass, type) and issubclass(Klass, NonCallableMock) and self.attribute): # 判断是否是类,是否是NonCallableMock子类,并且传入了mock的函数名称 _kwargs['name'] = self.attribute # 设置名称为mock的函数名 _kwargs.update(kwargs) # 更新传入参数 new = Klass(**_kwargs) # 实例化一个实例 if inherit and _is_instance_mock(new): # we can only tell if the instance should be callable if the # spec is not a list this_spec = spec if spec_set is not None: this_spec = spec_set if (not _is_list(this_spec) and not _instance_callable(this_spec)): Klass = NonCallableMagicMock _kwargs.pop('name') new.return_value = Klass(_new_parent=new, _new_name='()', **_kwargs) elif autospec is not None: # spec is ignored, new *must* be default, spec_set is treated # as a boolean. Should we check spec is not None and that spec_set # is a bool? if new is not DEFAULT: raise TypeError( "autospec creates the mock for you. Can't specify " "autospec and new." ) if original is DEFAULT: raise TypeError("Can't use 'autospec' with create=True") spec_set = bool(spec_set) if autospec is True: autospec = original new = create_autospec(autospec, spec_set=spec_set, _name=self.attribute, **kwargs) elif kwargs: # can't set keyword args when we aren't creating the mock # XXXX If new is a Mock we could call new.configure_mock(**kwargs) raise TypeError("Can't pass kwargs to a mock we aren't creating") new_attr = new # 赋值 self.temp_original = original # 保存旧函数到temp_original属性上 self.is_local = local # 保存是否在本地找到标志位 setattr(self.target, self.attribute, new_attr) # 设置属性值到module中,替换原来的属性值,此时本例中的mock_test就被替换为MagicMock类的实例 if self.attribute_name is not None: # 如果传入的属性值不为空 extra_args = {} if self.new is DEFAULT: # 判断是否为DEFAULT extra_args[self.attribute_name] = new # 设置到传入参数中 for patching in self.additional_patchers: arg = patching.__enter__() if patching.new is DEFAULT: extra_args.update(arg) return extra_args return new
此时就将app_name.apis中的命名为mock_test的函数的名称替换为了对应的MagicMock类实例,此时在调用mock_test函数时,就是调用的MagicMock的__call__方法,在详细查看MagicMock的继承关系后,可以知道最终会调用到CallableMixin类的__call__方法,
def _mock_check_sig(self, *args, **kwargs): # stub method that can be replaced with one with a specific signature passdef __call__(_mock_self, *args, **kwargs): # can't use self in-case a function / method we are mocking uses self # in the signature _mock_self._mock_check_sig(*args, **kwargs) # 检查 return _mock_self._mock_call(*args, **kwargs) # 执行
此时继续查看_mock_call方法,由于本例中只涉及到了简单的情况,故复杂的业务场景没有详细分析;
def _mock_call(_mock_self, *args, **kwargs): self = _mock_self self.called = True self.call_count += 1 # 调用次数加1 _new_name = self._mock_new_name # 获取mock的名称 _new_parent = self._mock_new_parent _call = _Call((args, kwargs), two=True) # 包装传入参数 self.call_args = _call self.call_args_list.append(_call) self.mock_calls.append(_Call(('', args, kwargs))) seen = set() skip_next_dot = _new_name == '()' do_method_calls = self._mock_parent is not None name = self._mock_name while _new_parent is not None: # 如果_new_parent不为空 this_mock_call = _Call((_new_name, args, kwargs)) if _new_parent._mock_new_name: dot = '.' if skip_next_dot: dot = '' skip_next_dot = False if _new_parent._mock_new_name == '()': skip_next_dot = True _new_name = _new_parent._mock_new_name + dot + _new_name if do_method_calls: if _new_name == name: this_method_call = this_mock_call else: this_method_call = _Call((name, args, kwargs)) _new_parent.method_calls.append(this_method_call) do_method_calls = _new_parent._mock_parent is not None if do_method_calls: name = _new_parent._mock_name + '.' + name _new_parent.mock_calls.append(this_mock_call) _new_parent = _new_parent._mock_new_parent # use ids here so as not to call __hash__ on the mocks _new_parent_id = id(_new_parent) if _new_parent_id in seen: break seen.add(_new_parent_id) ret_val = DEFAULT # 返回值默认设置为DEFAULT effect = self.side_effect # 获取effect值 if effect is not None: if _is_exception(effect): raise effect if not _callable(effect): result = next(effect) if _is_exception(result): raise result if result is DEFAULT: result = self.return_value return result ret_val = effect(*args, **kwargs) if (self._mock_wraps is not None and self._mock_return_value is DEFAULT): return self._mock_wraps(*args, **kwargs) if ret_val is DEFAULT: # 判断ret_val是否为默认值 ret_val = self.return_value # 如果是默认值则直接设置 为类的return_value值,在本例中在被设置为字符串"mock" return ret_val # 返回 "mock"
此时该mock的函数就返回了在测试中设置的字符串"mock",此时在测试api执行的过程中就返回了该值。在__enter__执行完成,就完成了对Mock函数的最终的执行,在执行完成之后,还需要调用__exit__进行最终的相关属性的恢复;
def __exit__(self, *exc_info): """Undo the patch.""" if not _is_started(self): raise RuntimeError('stop called on unstarted patcher') if self.is_local and self.temp_original is not DEFAULT: # 还原相关属性,将被Mock的属性还原为真是的函数 setattr(self.target, self.attribute, self.temp_original) else: delattr(self.target, self.attribute) # 先删除 if not self.create and (not hasattr(self.target, self.attribute) or self.attribute in ('__doc__', '__module__', '__defaults__', '__annotations__', '__kwdefaults__')): # needed for proxy objects like django settings setattr(self.target, self.attribute, self.temp_original) # 再还原 del self.temp_original # 删除相关属性值 del self.is_local del self.target for patcher in reversed(self.additional_patchers): if _is_started(patcher): patcher.__exit__(*exc_info)
至此,一个简单的测试的mock的过程就分析完成。本文基于了一个最简单的场景大致分析了该流程。
总结
本文简单的分析了mock该方法,在Django项目中的最简单的使用场景,mock的实现原理可以简单的描述为,通过导入相关模块,然后通过更新该模块对应的该函数的实例,通过该实例在被其他函数调用时,直接更改为我们期望的返回值,以此达到模拟假数据的返回,如有疏漏请批评指正。
转载地址:https://blog.csdn.net/qq_33339479/article/details/84578544 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!