天天看点

mock使用笔记1. 什么情况下使用mock2. 安装3. Mock实例4. patch()5. 常见问题6. 避免常见的问题7. 总结参考

文章目录

  • 1. 什么情况下使用mock
  • 2. 安装
  • 3. Mock实例
    • 惰性属性和方法
    • 断言和检查
    • 管理Mock的返回值
    • 管理Mock的副作用
    • 配置Mock
  • 4. patch()
    • patch()作为装饰器
    • patch()作为上下文管理器
    • 使用补丁模拟对象的属性
    • 补丁打在何处
  • 5. 常见问题
  • 6. 避免常见的问题
  • 7. 总结
  • 参考

1. 什么情况下使用mock

  • 减少测试运行时间, 只关注测试目标
  • 与外部接口交互时, 接口格式数据是否正确, 不需要得到实际的数据
  • 导入外部模块时, 只关注相应的函数是否被调用
  • 测试条件语句和异常处理语句能够正常工作
  • 测试代码块的逻辑

2. 安装

  • unittest.mock 不需要安装, 系统内置
  • pytest-mock 需要安装, pytest的插件

3. Mock实例

from unittest.mock import Mock                                                                                                  

mock = Mock()                                                                                                                   
           

现在,可以用新的Mock替换代码中的对象。可以通过将其作为参数传递给函数或重新定义另一个对象来完成此操作:

import json

def do_something(arg):
	pass

# Pass mock as an argument to do_something()
do_something(mock)

# Patch the json library
json = mock
           

惰性属性和方法

Mock必须模拟它所替换的任何对象。为了实现这种灵活性,它会在您访问它们时创建其属性:

>>> mock.some_attribute
<Mock name='mock.some_attribute' id='4394778696'>
>>> mock.do_something()
<Mock name='mock.do_something()' id='4394778920'>
           

由于Mock可以动态创建任意属性,因此它适合替换任何对象。

使用前面的示例,如果您要模拟json库并调用dumps(),Python mock对象将创建该方法,以便其接口可以与库的接口匹配:

>>> json = Mock()
>>> json.dumps()
<Mock name='mock.dumps()' id='4392249776'>
           

注意这个dumps()模拟版本的两个关键特性:

  1. 与实际的dumps()不同,此模拟方法不需要参数。事实上,它会接受你传递给它的任何参数。
  2. dumps()的返回值也是一个Mock。Mock递归定义其他Mock的功能允许您在复杂的情况下使用Mock
>>> json = Mock()
>>> json.loads('{"k": "v"}').get('k')
<Mock name='mock.loads().get()' id='4379599424'>
           

断言和检查

模拟实例存储关于如何使用它们的数据。例如,您可以看到是否调用了一个方法,如何调用该方法,等等。

首先,您可以断言程序按预期使用了一个对象:

>>> from unittest.mock import Mock

>>> # Create a mock object
... json = Mock()

>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4550144184'>

>>> # You know that you called loads() so you can
>>> # make assertions to test that expectation
... json.loads.assert_called()    # 检查是否调用了loads模拟方法
>>> json.loads.assert_called_once()    # 检查是否只调用了一次loads模拟方法
>>> json.loads.assert_called_with('{"key": "value"}')    # 检查是否使用参数'{"key": "value"}'来调用loads方法
>>> json.loads.assert_called_once_with('{"key": "value"}')    # 检查是否使用参数'{"key": "value"}'来调用了一次loads方法
# 先检查是否调用, 后检查参数

>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4550144184'>

>>> # If an assertion fails, the mock will raise an AssertionError
... json.loads.assert_called_once()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 795, in assert_called_once
    raise AssertionError(msg)
AssertionError: Expected 'loads' to have been called once. Called 2 times.

>>> json.loads.assert_called_once_with('{"key": "value"}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 824, in assert_called_once_with
    raise AssertionError(msg)
AssertionError: Expected 'loads' to be called once. Called 2 times.

>>> json.loads.assert_not_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 777, in assert_not_called
    raise AssertionError(msg)
AssertionError: Expected 'loads' to not have been called. Called 2 times.
           

要通过这些断言, 需要使用与调用模拟方法相同的参数

>>> json.loads(s='{"key": "value"}')     # 实际调用关键字参数
>>> json.loads.assert_called_with('{"key": "value"}')    # 预期调用位置参数
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 814, in assert_called_with
    raise AssertionError(_error_message()) from cause
AssertionError: Expected call: loads('{"key": "value"}')
Actual call: loads(s='{"key": "value"}')
>>> json.loads.assert_called_with(s='{"key": "value"}')
           

其次,您可以查看特殊属性以了解应用程序如何使用对象:

>>> from unittest.mock import Mock

>>> # Create a mock object
... json = Mock()
>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4391026640'>

>>> # Number of times you called loads():
... json.loads.call_count
1
>>> # The last loads() call:
... json.loads.call_args
call('{"key": "value"}')
>>> # List of loads() calls:
... json.loads.call_args_list    # 列出所有的loads调用
[call('{"key": "value"}')]
>>> # List of calls to json's methods (recursively):
... json.method_calls    # 列出json调用过的所有方法
[call.loads('{"key": "value"}')]
           

可以使用这些属性编写测试,以确保对象的行为符合预期。

管理Mock的返回值

使用模拟的一个原因是在测试期间控制代码的行为。一种方法是指定函数的返回值。

# my_calendar.py

from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

# Test if today is a weekday
assert is_weekday()    # 结果取决于运行测试的日期
           

在编写测试时,确保结果是可预测的很重要。可以使用Mock在测试期间消除代码中的不确定性。

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()

def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

# Mock .today() to return Tuesday
datetime.datetime.today.return_value = tuesday
# Test Tuesday is a weekday
assert is_weekday()
# Mock .today() to return Saturday
datetime.datetime.today.return_value = saturday
# Test Saturday is not a weekday
assert not is_weekday()
           

freezegun是一个用来模拟时间的库。

有时,当您多次调用函数或引发异常时,您可能希望使函数返回不同的值。

管理Mock的副作用

可以通过指定模拟函数的副作用来控制代码的行为。

使用副作用抛出异常

# my_calendar.py
import unittest
from requests.exceptions import Timeout
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def test_get_holidays_timeout(self):
        # Test a connection timeout
        requests.get.side_effect = Timeout
        with self.assertRaises(Timeout):
            get_holidays()

if __name__ == '__main__':
    unittest.main()
           

函数副作用(side effects)会覆盖返回值(return_value)。

import requests
import unittest
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def log_request(self, url):
        # Log a fake request for test output purposes
        print(f'Making a request to {url}.')
        print('Request received!')

        # Create a new Mock to imitate a Response
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            '12/25': 'Christmas',
            '7/4': 'Independence Day',
        }
        return response_mock

    def test_get_holidays_logging(self):
        # Test a successful, logged request
        requests.get.side_effect = self.log_request
        # get函数会传递参数到log_request函数, 并接受log_request函数的返回值,并返回这个返回值
        assert get_holidays()['12/25'] == 'Christmas' 

if __name__ == '__main__':
    unittest.main()
           

side_effect可以是一个可迭代对象(iterable)。iterable必须由返回值、异常或两者的混合组成。每次调用模拟方法时,iterable都会生成下一个值。

import unittest
from requests.exceptions import Timeout
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def test_get_holidays_retry(self):
        # Create a new Mock to imitate a Response
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            '12/25': 'Christmas',
            '7/4': 'Independence Day',
        }
        # Set the side effect of .get()
        requests.get.side_effect = [Timeout, response_mock]
        # Test that the first request raises a Timeout
        with self.assertRaises(Timeout):
            get_holidays()
        # Now retry, expecting a successful response
        assert get_holidays()['12/25'] == 'Christmas'
        # Finally, assert .get() was called twice
        assert requests.get.call_count == 2

if __name__ == '__main__':
    unittest.main()
           

配置Mock

可以在创建一个Mock实例或者使用configure_mock来设置对象的一些行为。

>>> mock = Mock(side_effect=Exception)
>>> mock()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 939, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 995, in _mock_call
    raise effect
Exception

>>> mock = Mock(name='Real Python Mock')
>>> mock
<Mock name='Real Python Mock' id='4434041432'>

>>> mock = Mock(return_value=True)
>>> mock()
True
           

其它的一些属性, 比如name只能在创建Mock实例或者使用configure_mock()来设置它的值。name是要使用的对象的通用属性。因此,Mock不允许在实例上设置该值 。如果直接通过mock实例访问或设置name, 将创建一个.name属性,而不是配置mock。

>>> mock = Mock(name='Real Python Mock')
>>> mock.name    # 创建一个name属性, 默认情况下值为Mock对象
<Mock name='Real Python Mock.name' id='4434041544'>

>>> mock = Mock()
>>> mock.name = 'Real Python Mock'	# 创建一个name属性
>>> mock.name
'Real Python Mock'
>>> mock
<Mock id='139792928989424'>
           

使用

.configure_mock()

来配置已经存在的

Mock

实例

>>> mock = Mock()
>>> mock.configure_mock(return_value=True)
>>> mock()
True
           

通过解包字典来配置

Mock

holidays = {'12/25': 'Christmas', '7/4': 'Independence Day'}
response_mock = Mock(**{'json.return_value': holidays})
           

4. patch()

patch()

在给定的模块中查找对象并用Mock替换该对象。

patch()作为装饰器

如果需要在整个函数的执行过程中都模拟一个对象, 就是用装饰器

# my_calendar.py
import requests
from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

           

需要在测试函数中定义一个新的参数,

patch()

通过这个参数将模拟对象传到测试函数中。

patch()

返回的是

Mock

的子类

MagicMock

实例。

MagicMock

实现了很多神奇的方法。

# test.py
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    @patch('my_calendar.requests')
    def test_get_holidays_timeout(self, mock_requests):
            mock_requests.get.side_effect = Timeout
            with self.assertRaises(Timeout):
                get_holidays()
                mock_requests.get.assert_called_once()

if __name__ == '__main__':
    unittest.main()
           

patch()作为上下文管理器

如下情况,可能更适合上下文管理器。

  • 只是在测试的部分区域使用模拟的对象
  • 使用了太多的装饰器或者参数,导致测试的可读性变差
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    def test_get_holidays_timeout(self):
        with patch('my_calendar.requests') as mock_requests:
            mock_requests.get.side_effect = Timeout
            with self.assertRaises(Timeout):
                get_holidays()
                mock_requests.get.assert_called_once()

if __name__ == '__main__':
    unittest.main()
           

使用补丁模拟对象的属性

可以使用

path.object()

来模拟对象的一个方法。

object

不使用目标路径,而是对象本身作为第一个参数,第二个参数是需要模拟的属性或方法。其它参数与

path

相同。

比如,

.test_get_holidays_timeout()

实际上仅仅需要模拟

requests.get()

和设置它的

.side_effect

Timeout

requests

其它的属性和方法不变。

import unittest
from my_calendar import requests, get_holidays
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    @patch.object(requests, 'get', side_effect=requests.exceptions.Timeout)
    def test_get_holidays_timeout(self, mock_requests):
            with self.assertRaises(requests.exceptions.Timeout):
                get_holidays()

if __name__ == '__main__':
    unittest.main()
           

补丁打在何处

例子1.

.is_weekday

来自

my_calendar

模块

>>> import my_calendar
>>> from unittest.mock import patch

>>> with patch('my_calendar.is_weekday'):
...     my_calendar.is_weekday()
...
<MagicMock name='is_weekday()' id='4336501256'>
           

例子2.

is_weekday

来自本地作用域

>>> from my_calendar import is_weekday
>>> from unittest.mock import patch

# 模拟的是my_calendar模块的is_weekday, 本地的is_weekday不受影响
>>> with patch('my_calendar.is_weekday'):   
...     is_weekday()    # 使用is_weekday的本地引用
...
False
           

例子3. 模拟本地的

is_weekday

>>> from unittest.mock import patch
>>> from my_calendar import is_weekday

>>> with patch('__main__.is_weekday'):
...     is_weekday()
...
<MagicMock name='is_weekday()' id='4502362992'>
           

5. 常见问题

  • 重命名了一个方法,但忘记了修改测试中调用了

    .assert_not_called()

    的模拟方法。

    .assert_not_called()

    仍然为

    True

    。但是,断言没有用处,因为该方法已不存在。
  • 当调用

    .asert_called()

    代替

    .assert_called()

    , 测试中不会抛出

    AssertionError

    . 因为当拼写错误时, 会在

    Mock

    对象上创建一个名字为

    .asert_called()

    的新方法。如果拼写错误的属性或方法以

    assert

    assret

    开头,

    Mock

    会自动抛出

    AttributeError

    异常。
  • 当模拟系统外部的API时, 当外部API改变时, 测试会通过,但实际上测试中模拟外部API的部分已经无效。生产环境会报错。

6. 避免常见的问题

出现这些问题是因为Mock在访问不存在的属性和方法时会创建它们。这些问题的答案是防止Mock创建模拟对象不存在的方法和属性。

配置

Mock

时,可以将对象规范传递给spec参数。spec参数接受名称列表或对象,并定义

Mock

对象的接口。如果试图访问不属于该规格的属性,

Mock

将抛出

AttributeError

异常.

>>> calendar = Mock(spec=['is_weekday', 'get_holidays'])

>>> calendar.is_weekday()
<Mock name='mock.is_weekday()' id='4569015856'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'
           

使用对象来配置

Mock

的规格

>>> import my_calendar
>>> from unittest.mock import Mock

>>> calendar = Mock(spec=my_calendar)
>>> calendar.is_weekday()
<Mock name='mock.is_weekday()' id='4569435216'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'
           

使用

create_autospec

实现自动规格化

>>> from unittest.mock import create_autospec

>>> calendar = create_autospec(my_calendar)
>>> calendar.is_weekday()
<MagicMock name='mock.is_weekday()' id='4579049424'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'
           

patch()

中实现相同的效果

>>> import my_calendar
>>> from unittest.mock import patch

>>> with patch('__main__.my_calendar', autospec=True) as calendar:
...     calendar.is_weekday()
...     calendar.create_event()
...
<MagicMock name='my_calendar.is_weekday()' id='4579094312'>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'
           

7. 总结

  • 使用Mock来模拟测试中的对象
  • 检查使用数据以了解如何使用对象
  • 自定义模拟对象的返回值和副作用
  • 在整个代码库中的使用patch()对象
  • 查看并避免使用Python模拟对象的问题

参考

Understanding the Python Mock Object Library

An Introduction to Mocking in Python

What the mock? — A cheatsheet for mocking in Python

Getting Started with Mocking in Python

pytest: How to mock in Python

在单元测试中给对象打补丁

unittest.mock

pytest-mock