Django项目test中的mock概述
发布日期:2021-07-25 13:04:49 浏览次数:11 分类:技术文章

本文共 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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Django项目日志概述
下一篇:djangorestframework源码分析2:serializer序列化数据的执行流程

发表评论

最新留言

关注你微信了!
[***.104.42.241]2024年04月14日 09时29分26秒