• Python内置库:unittest.mock(单元测试mock的基础使用)


    1. 为什么需要使用mock

    unittest.mock是用于在单元测试中模拟和替换指定的对象及行为,以便测试用例更加准确地进行测试运行。例如对于以下代码,想要针对函数func_a写一个简单的单元测试:

    import unittest
    
    
    def func_c(arg1, arg2):
        a_dict = {}
        # 其他代码
        return a_dict
    
    
    def func_b(arg3, arg4):
        b_list = []
        a_arg1 = None
        a_arg2 = None
        # 其他代码
        a_dict = func_c(a_arg1, a_arg2)
        # 其他代码
        return b_list
    
    
    def func_a():
        b_list = func_b('111', '222')
        if 'aaa' in b_list:
            return False
    
        return True
    
    
    class FuncTest(unittest.TestCase):
        def test_func_a(self):
            assert func_a()
    

    但是这样的话,函数func_b和func_c的逻辑都需要一起测试,在单元测试中这明显是不合理的,对于想要测试的函数func_a,里面所使用到的其他函数或接口,我们只需要关心它的返回值即可,保证当前测试的函数按它自己的逻辑运行,所以可以写成下面这样:

    import unittest
    
    
    def mock_func_b(arg3, arg4):
        return ['bbb', 'ccc']
    
    
    def func_a():
        # 使用一个模拟的mock_func_b代替真正的函数func_b
        # 这个mock_func_b不需要关心具体实现逻辑,只关心返回值
        b_list = mock_func_b('111', '222')
        if 'aaa' in b_list:
            return False
    
        return True
    
    
    class FuncTest(unittest.TestCase):
        def test_func_a(self):
            assert func_a()
    

    注意,模拟的mock_func_b并不需要保证func_a中所有的可能分支和逻辑都执行一次,单元测试更多的是验证函数或接口(比如这里的func_a)是否与设计相符、发现代码实现与需求中存在的错误、修改代码时是否引入了新的错误等。但是这里的写法也有很大的问题,一个功能模块中使用的函数或接口通常来讲其实并不少、也没有这里这么简单,如果涉及的接口都要重新写一个mock对象(如mock_func_b),那单元测试的工作将会变得非常繁重和复杂,所以unittest中的mock模块派上了用场,这个模块也正如它的名称一样,可以模拟各种对象。

    import unittest
    from unittest import mock
    
    
    def func_a():
        # 创建一个mock对象,return_value表示在该对象被执行时返回指定的值
        mock_func_b = mock.Mock(return_value=['bbb', 'ccc'])
        b_list = mock_func_b('111', '222')
        if 'aaa' in b_list:
            return False
    
        return True
    
    
    class FuncTest(unittest.TestCase):
        def test_func_a(self):
            assert func_a()
    

    2. Mock对象

    2.1 快速上手

    mock模块中的Mock类最常用的就是Mock和MagicMock,可以用来模拟对象、属性和方法,并且会保存这些被模拟的对象的使用细节,之后再使用断言来判断它们是否按照期待的被使用。

    使用Mock类指定其被调用时触发的一些行为(Mock对象也可以用于替换指定的对象或方法)。

    >>> from unittest.mock import MagicMock, Mock
    >>> mock = Mock(side_effect=KeyError('foo'))
    >>> mock()  # 直接调用将发生指定的异常
    Traceback (most recent call last):
      ...
    KeyError: 'foo'
    >>> values = {'a': 1, 'b': 2, 'c': 3}
    >>> def side_effect_func(arg):
    ...     return values[arg]
    ... 
    >>> mock.side_effect = side_effect_func  # 重新指定side_effect
    >>> mock('a'), mock('b'), mock('c')  # 表示只能传入指定的参数
    (1, 2, 3)
    >>> mock('a'), mock('b'), mock('c'), mock('d')  # 传入未指定的参数则会报错
    Traceback (most recent call last):
      ...
    KeyError: 'd'
    >>> mock.side_effect = [5, 4, 3, 2, 1]  # 重新指定side_effect
    >>> mock(), mock(), mock(), mock()  # 相当于迭代器,依次返回对应的值,使用完后再次调用就会报错
    (5, 4, 3, 2)
    >>> mock()
    1
    >>> mock()
    Traceback (most recent call last):
      ...
    StopIteration
    

    使用spec参数指定Mock对象的属性和方法,指定时可以是一个对象,会自动将该对象的属性和方法赋给当前Mock对象,但是注意赋值的属性和方法也是Mock类型的,并不会真正执行对应方法的内容。

    from unittest.mock import MagicMock, Mock
    
    
    class SpecMock:
        def test_spec(self):
            print('spec running...')
    
    
    def test_mock_spec():
        mock = Mock(spec=SpecMock())
        print(mock.test_spec)  # 注意打印的内容,返回的是一个Mock类型
        print(mock.test_spec())  # 该方法内的内容并没有被执行
        mock.func()
    
    
    if __name__ == '__main__':
        test_mock_spec()
    
        
    '''输出:
    <Mock name='mock.test_spec' id='1956426692808'>
    <Mock name='mock.test_spec()' id='1956430210952'>
    Traceback (most recent call last):
      ...
    AttributeError: Mock object has no attribute 'func'
    '''
    

    使用MagicMock创建并替换原有的方法。

    from unittest.mock import MagicMock
    
    
    class TestClass:
        def func(self, a, b):
            return a + b
    
    
    tc = TestClass()
    # 使用MagicMock创建并替换原来的func方法,并指定其被调用时的返回值
    tc.func = MagicMock(return_value='666')
    print(tc.func(2, 3))
    # 判断func是否按照指定的方式被调用,如果没有,
    # 比如这里指定assert_called_with(4, 5),就会抛出异常,
    # 因为之前使用的是tc.func(2, 3)来进行调用的
    print(tc.func.assert_called_with(2, 3))
    
    '''输出:
    666
    None
    '''
    

    Mock类虽然支持对Python中所有的magic方法进行“mock”,并允许给magic方法赋予其他的函数或者Mock实例,但是如果需要使用到magic方法,最简单的方式是使用MagicMock类,它继承自Mock并实现了所有常用的magic方法。

    >>> from unittest.mock import MagicMock, Mock, patch
    >>> mock = Mock()
    >>> mock.__str__ = Mock(return_value='666')
    >>> str(mock)
    '666'
    >>> m_mock = MagicMock()
    >>> m_mock.__str__.return_value = '999'
    >>> str(m_mock)
    '999'
    >>> m_mock.__str__.assert_called_with()
    

    2.2 Mock类和MagicMock类

    Mock对象可以用来模拟对象、属性和方法,Mock对象也会记录自身被使用的过程,你可以通过相关assert方法来测试验证代码是否被执行过。MagicMock类是Mock类的一个子类,它实现了所有常用的magic方法。

    2.2.1 Mock构造函数

    构造函数 unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs) 参数解释:

    • spec: 可以传入一个字符串列表、类或者实例,如果传入的是类或者实例对象,那么将会使用 dir 方法将该类或实例转化为一个字符串列表(magic属性和方法除外)。访问(get操作)任何不在此列表中的属性和方法时都会抛出AttributeError。如果传入的是一个类或者实例对象,那么__class__方法会返回对应的类,以便在使用 isinstance 方法时进行判断。
    • spec_set: spec参数的变体,但更加严格,如果试图使用get操作或set操作来操作此参数指定的对象中没有的属性或方法,则会抛出AttributeError。spec参数是可以对spec指定对象中没有的属性进行set操作的。参考 mock_add_spec 方法。
    • side_effect: 可以传入一个函数,每次当Mock对象被调用的时候,就会自动调用该函数,可以用于抛出异常或者动态改变mock对象的返回值,此函数使用的参数与mock对象被调用时传入的参数是一样的,并且,除非它的返回值为 unittest.mock.DEFAULT 对象,否则这个函数的返回值将会作为mock对象的返回值。也可以传入一个exception对象或者实例对象,如果传入exception对象,则每次调用mock对象都会抛出该异常。也可以传入一个可迭代对象,每次调用mock对象时就会返回该迭代对象的下一个值。如果不想使用了,可以将它设置为None。具体参见后面mock对象 side_effect 属性的使用。
    • return_value: 每次调用mock对象时的返回值,默认第一次调用时创建新的Mock对象。
    • unsafe: 如果某个属性或方法中会assert一个AttributeError,则可以设置 unsafe=True 来跳过这个异常。(Python3.5更新)
    • wraps: 包裹Mock对象的对象,当wraps不为None时,会将Mock对象的调用传入wraps对象中,并且可以通过Mock对象访问wraps对象中的属性。但是如果Mock对象指定了明确的return_value那么wraps对象就不会起作用了。
    • name: 指定mock对象的名称,可在debug的时候使用,并且可以“传播”到子类中。
    • 注: 初始化Mock对象时,还可以传入其他任意的关键字参数,这些参数会被用于设置成Mock对象的属性,具体参见后面的 configure_mock()

    2.2.2 常用方法

    assert_called()

    assert:mock对象至少被调用过一次。(Python3.6新增)

    assert_called_once()

    assert:mock对象只被调用过一次。(Python3.6新增)

    assert_called_with(*args, **kwargs)

    assert:mock对象最后一次被调用的方式。

    >>> from unittest.mock import Mock
    >>> mock = Mock()
    >>> mock.method(1, 2, 3, test='wow')
    <Mock name='mock.method()' id='2956280756552'>
    >>> mock.method.assert_called_with(1, 2, 3, test='wow')
    

    assert_called_once_with(*args, **kwargs)

    assert:mock对象以指定方式只被调用过一次。

    assert_any_call(*args, **kwargs)

    assert:mock对象以指定方式被调用过。

    assert_has_calls(calls, any_order=False)

    calls是一个 unittest.mock.call 对象列表,any_order默认为False,表示calls中的对象必须按照原来的调用顺序传入,为True则表示可以是任意顺序。

    assert:mock对象以calls中指定的调用方式被调用过。

    from unittest.mock import Mock, call
    >>> mock = Mock(return_value=None)
    >>> mock(1)
    >>> mock(2)
    >>> mock(3)
    >>> mock(4)
    >>> calls = [call(2), call(3)]
    >>> mock.assert_has_calls(calls)
    >>> calls = [call(4), call(2), call(3)]
    >>> mock.assert_has_calls(calls, any_order=True)
    

    assert_not_called()

    assert:mock对象没有被调用过。(Python3.5新增)

    reset_mock(*, return_value=False, side_effect=False)

    重置所有调用相关的属性,但是默认不会改变它的return_value和side_effect,以及其他属性。
    注:return_value和side_effect是两个关键字参数,并且是在Python3.6才增加的。

    >>> from unittest.mock import Mock
    >>> mock = Mock(return_value='hi')
    >>> mock('hello')
    'hi'
    >>> mock.called
    True
    >>> mock.reset_mock()
    >>> mock.called
    False
    

    mock_add_spec(spec, spec_set=False)

    spec参数可以是一个对象或者一个字符串列表,如果指定了此参数,那么只有spec指定的属性才可以进行访问(get操作)。如果spec_set设置为True,那么只有spec中指定的属性才可以进行set操作。

    >>> mock = Mock()
    >>> mock.mock_add_spec(spec=['test_spec'])
    >>> mock.test_spec
    <Mock name='mock.test_spec' id='1504477311816'>
    >>> mock.new_test_spec  # 只能访问spec指定的属性
    Traceback (most recent call last):
      ...
    AttributeError: Mock object has no attribute 'new_test_spec'
    >>> mock.new_test_spec = 'test spec!!!'  # 但是可以设置新的属性
    >>> mock.new_test_spec
    'test spec!!!'
    >>> mock.mock_add_spec(spec=['test_spec'], spec_set=True)
    >>> mock.new_test_spec3 = 'test spec3'  # spec_set设置为True后,将不能设置新的属性
    Traceback (most recent call last):
      ...
    AttributeError: Mock object has no attribute 'new_test_spec3'
    

    attach_mock(mock, attribute)

    将一个mock对象作为一个子属性添加到当前mock对象,并且会将其name值和parent关系进行替换。注意,此方法的调用会被记录在 method_calls 方法和 mock_calls 方法中。

    configure_mock(**kwargs)

    添加额外的属性到已经创建的mock对象,并且可以给属性添加return_value值和side_effect值。在创建mock对象时也可以用这种方式添加额外的属性。

    >>> from unittest.mock import Mock
    >>> mock = Mock()
    >>> attrs = {'func.return_value': 'hello', 'side_func.side_effect': ValueError}
    >>> mock.configure_mock(**attrs)  # 给已经创建的mock对象添加额外的属性
    >>> mock.func()
    'hello'
    >>> mock.side_func()
    Traceback (most recent call last):
      ...
    ValueError
    >>> new_mock = Mock(other_attr='hi', **attrs)  # 在创建mock对象时指定额外的属性,效果同configure_mock()方法
    >>> new_mock.other_attr
    'hi'
    >>> new_mock.func()
    'hello'
    >>> new_mock.side_func()
    Traceback (most recent call last):
      ...
    ValueError
    

    called

    如果mock对象被调用过则返回True,否则返回False。

    >>> mock = Mock(return_value=None)
    >>> mock.called
    False
    >>> mock()
    >>> mock.called
    True
    

    call_count

    返回mock对象被调用的次数。

    >>> mock = Mock(return_value=None)
    >>> mock.call_count
    0
    >>> mock()
    >>> mock()
    >>> mock.call_count
    2
    

    return_value

    指定mock对象被调用时的返回值,也可以在创建mock对象时通过参数进行指定。如果没有进行指定,return_value的默认值为一个mock对象,而且它就是一个正常的mock对象,你可以把它当成普通的mock对象进行其他操作。

    >>> mock = Mock(return_value='hello')
    >>> mock()
    'hello'
    >>> mock.return_value = 'hi'
    >>> mock()
    'hi'
    >>> new_mock = Mock()
    >>> new_mock.return_value
    <Mock name='mock()' id='2064061578056'>
    

    side_effect

    这个属性可以是函数、可迭代对象或者异常(类或实例都可以),当mock对象被调用时, side_effect 属性对应的对象就会被调用一次。
    如果传入的是函数,那么它将在mock对象调用时被执行,且执行时此函数传入的参数与mock对象被调用时的参数是一致的,此函数的返回值即mock被对象调用的返回值,但是如果函数的返回值是 unittest.mock.DEFAULT 对象,那么mock对象被调用的返回值就是它自身的return_value属性值。
    如果传入的是一个可迭代对象,那么这个对象将被用作产生一个迭代器,这个迭代器在每一次mock对象被调用时返回一个值,这个值可以是异常类的实例,也可以是一个普通的值,当然如果这个返回值是一个 unittest.mock.DEFAULT 对象,则返回mock对象本身的return_value属性值。

    side_effect 是一个异常:

    >>> from unittest.mock import Mock
    >>> mock = Mock()
    >>> mock.side_effect = ValueError('hello')
    >>> mock()
    Traceback (most recent call last):
      ...
    ValueError: hello
    

    side_effect 是一个可迭代对象:

    >>> mock.side_effect = [1, 2, 3]
    >>> mock()
    1
    >>> mock()
    2
    >>> mock()
    3
    >>> mock()
    Traceback (most recent call last):
      ...
    StopIteration
    

    side_effect 是一个 unittest.mock.DEFAULT

    >>> from unittest.mock import DEFAULT, Mock
    >>> def side_func(*args, **kwargs):
    ...     return DEFAULT
    ... 
    >>> mock = Mock(return_value='hi')
    >>> mock.side_effect = side_func
    >>> mock()
    'hi'
    

    创建mock对象时指定 side_effect 为一个函数:

    >>> def side_func(value):
    ...     return value ** 2
    ... 
    >>> mock = Mock(side_effect=side_func)
    >>> mock(3)
    9
    

    side_effect 指定为None,即可清除该选项:

    >>> mock = Mock(side_effect=KeyError, return_value=3)
    >>> mock()
    Traceback (most recent call last):
      ...
    KeyError
    >>> mock.side_effect = None
    >>> mock()
    3
    

    call_args

    返回mock对象最近一次被调用时的参数,如果没有被调用过,则为None。
    也可以通过 call_args.argscall_args.kwargs 属性分别获取对应的参数。(Python3.8新增)

    >>> mock = Mock(return_value='hello')
    >>> print(mock.call_args)
    None
    >>> mock('aa', 'bb', hi='hi')
    'hello'
    >>> mock.call_args
    call('aa', 'bb', hi='hi')
    >>> isinstance(mock.call_args, tuple)
    True
    >>> mock.call_args == (('aa', 'bb'), {'hi': 'hi'})
    True
    

    call_args_list

    存储mock对象调用的列表,列表元素为call对象,在没有被调用之前为空列表。

    >>> from unittest.mock import Mock
    >>> mock = Mock(return_value=None)
    >>> mock.call_args_list
    []
    >>> mock(1, 2)
    >>> mock(arg1='hi', arg2='hello')
    >>> mock.call_args_list
    [call(1, 2), call(arg1='hi', arg2='hello')]
    >>> mock.call_args_list == [((1, 2), ), ({'arg1': 'hi', 'arg2': 'hello'}, )]
    True
    

    method_calls

    存储mock对象调用以及“调用的调用“的列表,列表元素为call对象,在没有被调用之前为空列表。

    >>> mock = Mock()
    >>> mock.method_calls
    []
    >>> mock.func()
    <Mock name='mock.func()' id='2152783337672'>
    >>> mock.pro.func2.attr()
    <Mock name='mock.pro.func2.attr()' id='2152784407496'>
    >>> mock.method_calls
    [call.func(), call.pro.func2.attr()]
    

    mock_calls

    存储mock对象所有类型调用的列表。

    >>> from unittest.mock import call, Mock
    >>> mock = Mock()
    >>> mock(1, 2, 3)
    <Mock name='mock()' id='2152784400584'>
    >>> result = mock.func(a=3)
    >>> result(44)
    <Mock name='mock.func()()' id='2152771939848'>
    >>> mock.top(a=3).bottom()
    <Mock name='mock.top().bottom()' id='2152784434888'>
    >>> mock.mock_calls
    [call(1, 2, 3),
     call.func(a=3),
     call.func()(44),
     call.top(a=3),
     call.top().bottom()]
    >>> mock.mock_calls[-1] == call.top(a=-1).bottom()  # 子调用bottom是没有记录其父调用top的参数的
    True
    

    class

    如果mock对象指定了spec对象,则会返回spec对象的类型,也可以直接赋值。这个属性主要是在 isinstance 进行判断的时候会用到。

    >>> mock = Mock(spec=3)
    >>> isinstance(mock, int)
    True
    >>> mock.__class__ = dict  # 如果不想特别去指定spec参数,可以直接进行赋值
    >>> isinstance(mock, dict)
    True
    

    2.3 其他Mock类

    2.3.1 NonCallableMock类

    unittest.mock.NonCallableMock 这是一个不可被调用的mock类,它的参数和Mock类的使用是一样的,不过 return_valueside_effect 这两个参数对 NonCallableMock 类来说是无意义的。

    2.3.2 PropertyMock类

    unittest.mock.PropertyMock 这是一个专门用于替换属性的Mock类,它提供了属性对应的get和set方法。

    from unittest.mock import patch, PropertyMock
    
    
    class Foo:
        @property
        def foo(self):
            return 'something'
    
        @foo.setter
        def foo(self, value):
            pass
    
    
    # 使用PropertyMock替换foo属性进行测试
    with patch('__main__.Foo.foo', new_callable=PropertyMock) as mock_foo:
        mock_foo.return_value = 'mockity-mock'
        this_foo = Foo()
        print(this_foo.foo)  # 调用foo的get方法
        this_foo.foo = 6  # 调用foo的set方法
    
        print(mock_foo.mock_calls)
    
    '''输出:
    mockity-mock
    [call(), call(6)]
    '''
    

    2.3.3 AsyncMock类 (Python3.8新增)

    unittest.mock.AsyncMock 一个MagicMock的异步版本,AsyncMock对象会像一个异步函数一样运行,它的调用的返回值是一个awaitable对象,这个awaitable对象返回 side_effect 或者 return_value 指定的值。

    >>> import asyncio
    >>> import inspect
    >>> from unittest.mock import AsyncMock
    >>> mock = AsyncMock()
    >>> asyncio.iscoroutinefunction(mock)
    True
    >>> inspect.isawaitable(mock())
    True
    

    如果Mock或者MagicMock的spec参数指定了一个异步的函数,那么对应mock对象的调用将返回一个协程对象。

    >>> from unittest.mock import MagicMock
    >>> async def async_func(): pass  # 注意async关键字是在Python3.7才有的
    ... 
    >>> mock = MagicMock(async_func)
    >>> mock
    <MagicMock spec='function' id='1934190100048'>
    >>> mock()
    <coroutine object AsyncMockMixin._execute_mock_call at 0x000001C2568E8EC0>
    

    如果Mock、MagicMock或者AsyncMock的spec参数指定了带有同步或者异步函数的类,那么对于Mock,所有的同步函数将被定义为Mock对象,对于MagicMock和AsyncMock,所有同步函数将被定义为MagicMock。而对于Mock、MagicMock或者AsyncMock,所有的异步函数都将被定义为AsyncMock对象。

    >>> class ExampleClass:
    ...     def sync_foo():
    ...         pass
    ...     async def async_foo():
    ...         pass
    ...     
    >>> a_mock = AsyncMock(ExampleClass)
    >>> a_mock.sync_foo
    <MagicMock name='mock.sync_foo' id='1934183952000'>
    >>> a_mock.async_foo
    <AsyncMock name='mock.async_foo' id='1934183974272'>
    >>> from unittest.mock import Mock
    >>> mock = Mock(ExampleClass)
    >>> mock.sync_foo
    <Mock name='mock.sync_foo' id='1934183980864'>
    >>> mock.async_foo
    <AsyncMock name='mock.async_foo' id='1934183978800'>
    

    assert_awaited()

    assert:mock对象至少被await过一次。注意,await的对象是被从mock对象中分离出来的,且该分离出来的对象必须被await关键字声明过才能进行assert判断。

    >>> mock = AsyncMock()
    >>> async def main(coroutine_mock):
    ...     await coroutine_mock
    ...     
    >>> coroutine_mock = mock()
    >>> mock.called
    True
    >>> mock.assert_awaited()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: Expected mock to have been awaited.
    >>> asyncio.run(main(coroutine_mock))
    >>> mock.assert_awaited()
    

    assert_awaited_once()

    assert:mock对象只被await了一次。

    >>> mock = AsyncMock()
    >>> async def main():
    ...     await mock()
    ...     
    >>> asyncio.run(main())
    >>> mock.assert_awaited_once()
    >>> asyncio.run(main())
    >>> mock.method.assert_awaited_once()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: Expected method to have been awaited once. Awaited 0 times.
    

    assert_awaited_with(*args, **kwargs)

    assert:mock对象最后一次的await的参数和指定的参数一致。

    >>> mock = AsyncMock()
    >>> async def main(*args, **kwargs):
    ...     await mock(*args, **kwargs)
    ...     
    >>> asyncio.run(main('foo', bar='bar'))
    >>> mock.assert_awaited_with('foo', bar='bar')
    >>> mock.assert_awaited_with('other')
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: expected await not found.
    Expected: mock('other')
    Actual: mock('foo', bar='bar')
    

    assert_awaited_once_with(*args, **kwargs)

    assert:mock对象只被await过一次,且使用的参数和指定的参数一致。

    >>> mock = AsyncMock()
    >>> async def main(*args, **kwargs):
    ...     await mock(*args, **kwargs)
    ...     
    >>> asyncio.run(main('foo', bar='bar'))
    >>> mock.assert_awaited_once_with('foo', bar='bar')
    >>> asyncio.run(main('foo', bar='bar'))
    >>> mock.assert_awaited_once_with('foo', bar='bar')
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: Expected mock to have been awaited once. Awaited 2 times.
    

    assert_any_await(*args, **kwargs)

    assert:mock对象以指定的参数await过。

    >>> mock = AsyncMock()
    >>> async def main(*args, **kwargs):
    ...     await mock(*args, **kwargs)
    ...     
    >>> asyncio.run(main('foo', bar='bar'))
    >>> asyncio.run(main('hello'))
    >>> mock.assert_any_await('foo', bar='bar')
    >>> mock.assert_any_await('other')
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: mock('other') await not found
    

    assert_has_awaits(calls, any_order=False)

    assert:mock对象以指定的call对象的调用方式await过。any_order用于指定是否需要判断call调用的顺序,默认需要判断。

    >>> mock = AsyncMock()
    >>> async def main(*args, **kwargs):
    ...     await mock(*args, **kwargs)
    ...     
    >>> from unittest.mock import call
    >>> calls = [call("foo"), call("bar")]
    >>> mock.assert_has_awaits(calls)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    AssertionError: Awaits not found.
    Expected: [call('foo'), call('bar')]
    Actual: []
    >>> asyncio.run(main('foo'))
    >>> asyncio.run(main('bar'))
    >>> mock.assert_has_awaits(calls)
    

    assert_not_awaited()

    assert:mock对象没有被await过。

    reset_mock(*args, **kwargs)

    Mock.reset_mock 使用相似,会将 await_count 置为0, await_args 置为None,清除 await_args_list 中的内容。

    await_count

    mock对象被await的次数。

    await_args

    mock对象最近一次被await的调用信息,是一个call对象。如果没有被await过,则为None。和 Mock.call_args 相似。

    >>> mock = AsyncMock()
    >>> async def main(*args):
    ...     await mock(*args)
    ...     
    >>> mock.await_args
    >>> asyncio.run(main('foo'))
    >>> mock.await_args
    ... call('foo')
    >>> asyncio.run(main('bar'))
    >>> mock.await_args
    call('bar')
    

    await_args_list

    是一个记录mock对象所有的await调用信息的列表,列表元素为call对象,初始值为空列表。

    >>> mock = AsyncMock()
    >>> async def main(*args):
    ...     await mock(*args)
    ...     
    >>> asyncio.run(main('foo'))
    >>> asyncio.run(main('bar'))
    >>> mock.await_args_list
    [call('foo'), call('bar')]
    

    2.4 Calling

    Mock对象每次调用都会返回 return_value 属性,默认的 return_value 是一个新的Mock对象,它会在第一次 return_value 被访问时创建,并且以后每次访问 return_value 都会返回第一次创建的Mock对象。

    2.4.1 call_args和call_args_list

    Mock的每次调用都会记录在 call_argscall_args_list 中。具体使用示例见之前的2.2.2章节。

    2.4.2 side_effect属性

    如果设置了 side_effect 属性,那么在调用时,会先记录此次调用信息,再去调用 side_effect 指定的对象。所以想要mock对象的调用抛出一个异常的最简单方式就是使用 side_effect 属性指定一个异常类或者异常实例。

    >>> m = MagicMock(side_effect=IndexError)
    >>> m(1, 2, 3)
    ...
    IndexError
    >>> m.mock_calls
    [...
     call(1, 2, 3),
     ...]
    >>> m.side_effect = KeyError('Bang!')
    >>> m('two', 'three', 'four')
    Traceback (most recent call last):
      ...
    KeyError: 'Bang!'
    >>> m.mock_calls
    [...
     call(1, 2, 3),
     ...
     call('two', 'three', 'four'),
     ...]
    

    如果 side_effect 是一个函数,那么调用mock对象的时候就会使用相同的参数去调用此函数。

    >>> m = MagicMock(side_effect=side_effect)
    >>> m(1)
    2
    >>> m(2)
    3
    >>> m.mock_calls
    [...,
     call(1),
     ...,
     call(2),
     ...]
    

    如果想要mock对象的调用返回一个默认值,那么可以有以下两种方式:在 side_effect 指定的函数中直接返回 mock.return_value ,或者返回 DEFAULT 对象。

    >>> from unittest.mock import DEFAULT
    >>> m = MagicMock()
    >>> # 方式一
    >>> def side_effect(*args, **kwargs):
    ...     return m.return_value
    ... 
    >>> m.side_effect = side_effect
    >>> m.return_value = 3
    >>> m()
    3
    >>> # 方式二
    >>> def side_effect(*args, **kwargs):
    ...     return DEFAULT
    ... 
    >>> m.side_effect = side_effect
    >>> m()
    3
    

    如果想要移除 side_effect 并返回mock的默认值,将它设置为None就可以了。

    >>> m = MagicMock(return_value=6)
    >>> def side_effect(*args, **kwargs):
    ...     return 3
    ... 
    >>> m.side_effect = side_effect
    >>> m()
    3
    >>> m.side_effect = None
    >>> m()
    6
    

    side_effect 的值也可以是可迭代对象,每次调用会依次获取可迭代对象中的下一个值,一直到可迭代对象的末尾,并触发StopIteration异常。如果可迭代对象中含有异常,当迭代到此异常时将会抛出该异常。

    >>> iterable = (33, ValueError, 66)
    >>> m = MagicMock(side_effect=iterable)
    >>> m()
    33
    >>> m()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    ValueError
    >>> m()
    66
    >>> m()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      ...
    StopIteration
    

    2.4.3 name属性

    如果想要设置mock对象的name属性,可以有两种方式:使用 mock.configure_mock(name='my_name') ,或者直接给mock对象赋值 mock.name='my_name'

    如果mock对象的属性是另一个mock对象时,这个属性的mock就相当于是父mock的子mock,子mock的调用会被记录在父mock的 method_callsmock_calls 中,如果你不想子mock的调用被记录,则可以在定义子mock时指定name属性,指定了name属性的子mock则不会被记录在父mock中。

    >>> parent = MagicMock()
    >>> child1 = MagicMock(return_value=None)
    >>> child2 = MagicMock(return_value=None)
    >>> parent.child1 = child1
    >>> parent.child2 = child2
    >>> child3 = MagicMock(name='child3')
    >>> parent.child3 = child3
    >>> child1(1)
    >>> child2(2)
    >>> child3(3)
    <MagicMock name='child3()' id='2247991039888'>
    >>> parent.mock_calls
    [...,
     call.child1(1),
     ...,
     call.child2(2),
     ...]
    

    如果需要将一个含有name属性的子mock对象赋给父mock,且可以记录子mock的调用,则需要使用attach_mock方法来将子mock赋给父mock。

    thing1 = object()
    thing2 = object()
    parent = MagicMock()
    with patch('__main__.thing1', return_value=None) as child1:
        with patch('__main__.thing2', return_value=None) as child2:
            # attach_mock第一个参数是mock对象,第二个参数是属性名,将mock对象当作属性赋给父mock对象
            parent.attach_mock(child1, 'child1')
            parent.attach_mock(child2, 'child2')
            child1('one')
            child2('two')
    
    print(parent.mock_calls)
    '''输出为
    [call.child1('one'), call.child2('two')]
    '''
    

    3. patch使用

    from unittest.mock import patch 可以用装饰器的方式对属性、方法和类进行装饰,或者在with上下文中使用,或者使用start和stop方法直接在代码中使用。使用patch的目的是在代码运行时将指定的对象变为执行mock对象,并且是在单元测试开始时就可以指定所有的mock对象,非常方便。

    3.1 快速上手

    可以使用patch装饰器替换某个模块的类,但是注意,导入时需要使用import导入对应的模块,也只能到模块这一级,函数中传参的顺序也必须是与装饰的顺序一致(从下到上)。

    # 只能导入到模块(文件和包)这一级,不能直接导入类
    # 这里的unittest_mock包下有一个test文件,本示例中对应的类都定义在这个文件中
    import unittest_mock.test
    
    
    # patch使用时传入对应类的路径字符串
    @patch('unittest_mock.test.PatchTest2')
    @patch('unittest_mock.test.PatchTest1')
    def patch_test(MockTest1, MockTest2):  # 注意这里的传参顺序是按照装饰的顺序(从下到上)来指定的
        unittest_mock.test.PatchTest1()  # 这里执行的已经不是真实的类了,而是一个MagicMock类
        unittest_mock.test.PatchTest2()
        assert MockTest1 is unittest_mock.test.PatchTest1  # 这里表明传入的参数和对应的类是相同的,都是MagicMock类
        assert MockTest2 is unittest_mock.test.PatchTest2
        assert MockTest1.called  # 表明这个类在这之前已经被调用了
        assert MockTest2.called
    
    
    if __name__ == '__main__':
        patch_test()
    

    可以使用with语法来使用 patch.object 装饰器。

    class PatchObjTest:
        def func(self, a, b, c):
            print(a, b, c)
    
    
    def test_patch_obj():
        with patch.object(PatchObjTest, 'func',
                          return_value='mock obj func...') as mock_func:
            patch_obj = PatchObjTest()
            print(patch_obj.func(1, 2, 3))
            mock_func.assert_called_once_with(1, 2, 3)
    
    
    if __name__ == '__main__':
        test_patch_obj()
        
    '''输出:
    mock obj func...
    '''
    

    可以会使用 patch.dict 替换原有的字典对象。

    def test_patch_dict():
        foo = {'key': 'value'}
        original = foo.copy()  # 浅拷贝
        # clear参数表示是否保留原有的项,True表示不保留, 默认保留
        with patch.dict(foo, {'new_key': 'new_value'}, clear=True):
            print(foo)
            assert foo == {'new_key': 'new_value'}
    
        print(foo)  # foo原本的值并没有被改变
        assert foo == original
    
    
    if __name__ == '__main__':
        test_patch_dict()
        
        
    '''输出:
    {'new_key': 'new_value'}
    {'key': 'value'}
    '''
    

    可以使用patch.create_autospec函数来创建所有和原对象一样的api。

    >>> from unittest.mock import create_autospec
    >>> def func(a, b, c):
    ...     pass
    ... 
    >>> mock_func = create_autospec(func, return_value='func autospec...')
    >>> func(1, 2, 3)
    >>> mock_func(1, 2, 3)
    'func autospec...'
    >>> mock_func(111)
    Traceback (most recent call last):
      ...
    TypeError: missing a required argument: 'b'
    

    3.2 patch使用

    unittest.mock.patch 可以作为一个函数装饰器,类装饰器,或者上下文管理器(with语句)。

    3.2.1 构造函数

    构造函数 unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs) 参数解释:

    • target:target参数是一个形如package.module.ClassName的字符串。target值将会被import并创建一个新的对象,所以target字符串必须是在当前环境可以import的。需要注意,被装饰的函数执行时,target的对象才会被创建,而不是运行装饰器的时候被创建。
    • new:如果没有指定,则对于async函数会创建一个AsyncMock对象,对于其他的,则会创建一个MagicMock对象。如果 patch() 是作为一个装饰器,且new参数没有指定,则创建的mock对象将会作为一个额外(即放在被装饰函数原有的参数之后)的参数传入被装饰的函数。如果 patch() 用在上下文管理器中,则创建的mock对象会被上下文管理器返回。
    • spec和spec_set:会当作参数传入MagicMock中。如果创建的是spec或spec_set对象,可以设置spec=True或者spec_set=True,以便让patch正常运行。
    • new_callable:可以是一个类或者一个callable对象,并会使用此参数创建一个对象,默认情况下,对于async函数会创建一个AsyncMock对象,对于其他的,则会创建一个MagicMock对象。
    • create:默认为False,如果指定为True,那么当patch的对象或函数不存在时会自动创建,当真正的对象在运行过程中被程序创建后就删除patch出来的mock对象,这个参数特别适用于一些运行时创建的内容。(Python3.5更新:如果想要patch的内容是 builtin 内建模块,则不用指定 create=True ,patch会在运行时自动创建。)

    3.2.2 基础使用

    patch可以作为一个装饰器为函数创建一个mock对象并传入被装饰的函数。如果patch装饰的是一个类,那么将会返回一个MagicMock对象,当这个类在test方法中被实例化时,那么将会返回此MagicMock对象的 return_value 值,注意,如果在一个test方法中实例化多次,也是返回的同一个对象,如果想要每次都返回新的不同的对象,那么可以使用 side_effect 参数。

    class SomeClass:
        pass
    
    @patch('__main__.SomeClass')
    def func(a, b, mock_someclass):
        print(a)
        print(b)
        print(mock_someclass)
    
    
    if __name__ == '__main__':
        func(2, 3)
    
    '''打印输出
    2
    3
    <MagicMock name='SomeClass' id='1519607444288'>
    '''
    

    如果mock了一个类,对该类的实例对象和真实的class进行 isinstance 判断,则需要指定 spec=True

    class Class:
        def method(self):
           pass
    
    def func():
        Original = Class
        patcher = patch('__main__.Class', spec=True)
        MockClass = patcher.start()
        instance = MockClass()
        # 如果不指定spec=True,则会抛出异常
        assert isinstance(instance, Original)
        patcher.stop()
    
    
    if __name__ == '__main__':
        func()
    

    patch默认创建的是MagicMock对象,如果想要创建一个指定的对象,就可以使用 new_callable 参数。甚至可以使用 new_callable 参数在test case中重定向输出。

    thing = object()
    with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing:
        assert thing is mock_thing
        thing()
    
    '''打印输出
    Traceback (most recent call last):
      ...
    TypeError: 'NonCallableMock' object is not callable
    '''
    
    from io import StringIO
    def foo():
        print('Something')
    
    @patch('sys.stdout', new_callable=StringIO)
    def test(mock_stdout):
        foo()
        assert mock_stdout.getvalue() == 'Something
    '
    
    test()
    

    patch中可以通过传参的方式给mock对象设置属性。

    >>> patcher = patch('__main__.thing', first='one', second='two')
    >>> mock_thing = patcher.start()
    >>> mock_thing.first
    'one'
    >>> mock_thing.second
    'two'
    

    可以通过字典的方式来配置mock对象的属性。

    >>> config = {'method.return_value': 3, 'other.side_effect': KeyError}
    >>> patcher = patch('__main__.thing', **config)
    >>> mock_thing = patcher.start()
    >>> mock_thing.method()
    3
    >>> mock_thing.other()
    Traceback (most recent call last):
      ...
    KeyError
    

    3.3 patch其他使用

    3.3.1 patch.object

    patch.object用来给对象(target参数)的成员(attribute参数)进行“mock”,其参数的用法和patch是一样的,且也可以使用参数的形式给创建的mock对象添加额外的属性。如果被装饰的对象是类的话,可以使用 patch.TEST_PREFIX 指定哪些方法需要被“mock”。

    patch.object被用来装饰一个函数的时候,那么被创建的mock对象会一个额外参数的形式传入被装饰的函数。

    @patch.object(SomeClass, 'class_method')
    def test(mock_method):
        SomeClass.class_method(3)
        mock_method.assert_called_with(3)
    
    test()
    

    3.3.2 patch.dict

    patch.dict 用来“mock”一个字典对象或者类似字典的对象,int_dict参数为需要“mock”的字典对象,也可以是一个可以通过import生成字典对象的字符串,values参数为创建的字典对象的内容,也可以是(key, value)形式的键值对。当test case结束后,原先的被mock的字典对象就会恢复。

    # 示例:直接mock字典对象
    foo = {}
    @patch.dict(foo, {'newkey': 'newvalue'})
    def test():
        assert foo == {'newkey': 'newvalue'}
    test()
    assert foo == {}
    
    # 示例:在类中mock字典对象
    import os
    import unittest
    from unittest.mock import patch
    @patch.dict('os.environ', {'newkey': 'newvalue'})
    class TestSample(unittest.TestCase):
        def test_sample(self):
            self.assertEqual(os.environ['newkey'], 'newvalue')
    
    # 示例:修改原本的字典对象
    foo = {}
    with patch.dict(foo, {'newkey': 'newvalue'}) as patched_foo:
        assert foo == {'newkey': 'newvalue'}
        assert patched_foo == {'newkey': 'newvalue'}
        # 可以往mock的字典中添加、删除、修改内容,当with上下文结束后,原先的foo就会恢复
        patched_foo['spam'] = 'eggs'
    
    assert foo == {}
    assert patched_foo == {}
    
    # 示例:mock内置模块的类似字典的对象
    import os
    with patch.dict('os.environ', {'newkey': 'newvalue'}):
        print(os.environ['newkey'])
    
    
    assert 'newkey' not in os.environ
    

    可以使用参数配置的方式给字典对象添加内容。

    mymodule = MagicMock()
    mymodule.function.return_value = 'fish'
    with patch.dict('sys.modules', mymodule=mymodule):
        import mymodule
        print(mymodule.function('some', 'args'))
    

    patch.dict 也支持一些类似字典但不是字典类型的对象,但是这些对象必须具有以下Magic方法: __getitem__()__setitem__()__delitem__() ,以及 __iter__()__contains__() 中的一个。

    class Container:
        def __init__(self):
            self.values = {}
        def __getitem__(self, name):
            return self.values[name]
        def __setitem__(self, name, value):
            self.values[name] = value
        def __delitem__(self, name):
            del self.values[name]
        def __iter__(self):
            return iter(self.values)
    
    thing = Container()
    thing['one'] = 1
    with patch.dict(thing, one=2, two=3):
        assert thing['one'] == 2
        assert thing['two'] == 3
    
    assert thing['one'] == 1
    assert list(thing) == ['one']
    

    3.3.3 patch.multiple

    patch.multiple 可以一次性创建多个mock对象,参数的用法和patch是一样的。

    使用patch.multiple创建多个mock对象时,需要使用 DEFAULT 对象。

    thing = object()
    other = object()
    
    # from unittest.mock import DEFAULT
    @patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
    def test_function(thing, other):  # 对于patch.multiple对应的参数,并没有特别顺序要求
        assert isinstance(thing, MagicMock)
        assert isinstance(other, MagicMock)
    
    test_function()
    

    也可以和patch作为装饰器一起使用,但是 patch.multiple 产生的额外参数传入被装饰的函数时需要放在patch的参数后面。

    thing = object()
    other = object()
    
    @patch('sys.exit')
    @patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
    def test_function(mock_exit, other, thing):  # 注意传入参数的顺序,other和thing必须在mock_exit后面,但是other和thing之间的顺序无所谓
        assert 'other' in repr(other)
        assert 'thing' in repr(thing)
        assert 'exit' in repr(mock_exit)
    
    test_function()
    

    如果 patch.multiple 在with中使用,则with返回的是一个字典对象。

    thing = object()
    other = object()
    
    with patch.multiple('__main__', thing=DEFAULT, other=DEFAULT) as values:
        assert 'other' in repr(values['other'])
        assert 'thing' in repr(values['thing'])
        assert values['thing'] is thing
        assert values['other'] is other
    

    3.3.4 patch的start和stop方法

    如果不想使用装饰器或with语法而直接使用patch,那么可以使用patch的start方法和stop方法。start方法能直接返回对应的mock对象,而stop方法则是取消使用patch,类似with语句的开始和结束。

    patcher = patch('package.module.ClassName')
    from package import module
    original = module.ClassName
    new_mock = patcher.start()
    assert module.ClassName is not original
    assert module.ClassName is new_mock
    patcher.stop()
    assert module.ClassName is original
    assert module.ClassName is not new_mock
    

    使用start和stop方法的另一个典型例子是test case的setUp和tearDown方法。

    class MyTest(unittest.TestCase):
        def setUp(self):
            self.patcher1 = patch('package.module.Class1')
            self.patcher2 = patch('package.module.Class2')
            self.MockClass1 = self.patcher1.start()
            self.MockClass2 = self.patcher2.start()
    
        def tearDown(self):
            self.patcher1.stop()
            self.patcher2.stop()
    
        def test_something(self):
            assert package.module.Class1 is self.MockClass1
            assert package.module.Class2 is self.MockClass2
    
    MyTest('test_something').run()
    

    调用了start后一定要记得调用stop,也可以在最后使用stopall方法一次性stop所有使用了start方法的patch对象。如果怕自己在最后忘记了调用stop方法,也可以在调用了start方法后,立即调用 unittest.TestCase.addCleanup() 方法,此方法会在最后自动调用stop。

    class MyTest(unittest.TestCase):
        def setUp(self):
            patcher = patch('package.module.Class')
            self.MockClass = patcher.start()
            self.addCleanup(patcher.stop)
    
        def test_something(self):
            assert package.module.Class is self.MockClass
    

    注: 此学习笔记大多是直接从官方文档翻译过来的https://docs.python.org/3/library/unittest.mock.html

  • 相关阅读:
    JavaWeb之Ajax快速入门(十九)
    JavaWeb之文件上传与下载&邮件技术(十八)
    JavaWeb之Filter快速入门(十七)
    JavaWeb之JDBC分页/监听器(十六)
    JavaWeb之JDBC&自定义框架/DBUtil(十五)
    JavaWeb之JDBC(十四)
    JavaWeb之JDBC学习(十三)
    JavaWeb之Mysql代码练习(十二)
    [ 校内OJ ] NOIP2019模拟赛(五)
    【一本通】欧拉回路
  • 原文地址:https://www.cnblogs.com/guyuyun/p/14880885.html
Copyright © 2020-2023  润新知