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()

可以使用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'

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'}
'''

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\n'

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

posted @ 2021-06-14 11:10  山上下了雪-bky  阅读(4815)  评论(0编辑  收藏  举报