python mock实践笔记

前言

如果你写代码时会写单元测试(unit test,UT),那么多半会遇到想要将某个函数隔离开,去掉外部依赖的情况,例如这个函数依赖其他函数的返回或者依赖某个API调用的返回。这种情况下就一定绕不开mock这项技术。本文并不打算介绍python下mock的方方面面,只会写我个人实际使用中觉得比较实用的部分。

mocking是什么?

mock就是模拟的意思,mocking主要用在单元测试中,当被测试的对象依赖另一个复杂的对象时,我们需要模拟这个复杂的对象的行为,mocking就是创建这个对象,模拟它的行为。

基本用法

unittest.mock模块包含了mock相关的功能。

Mock对象

看如下一段代码:

from unittest.mock import Mock
# 创建一个mock对象
mock = Mock()
print(mock)
# 结果:<Mock id='140668914327224'> 表明此处的mock是一个Mock对象

Mock非常灵活,当我们访问一个Mock对象的某个属性时,这个属性如果不存在会被自动创建,看如下代码:

# 在访问之前mock并没有some_attribute这个属性
print(mock.some_attribute)
# 结果:<Mock name='mock.some_attribute' id='140348173848360'> 
# 可见,在访问的时候创建了该属性

print(mock.do_something)
# 结果:<Mock name='mock.do_something' id='140348173886128'>

正是由于此特性,Mock可以用来模拟任意对象。

下面从最基本的开始介绍:

设置返回值和属性

Mock对象可以返回常量,也可以随着输入返回不同值。

返回常量-return_value

mock = Mock()
# 设置返回值
mock.return_value = 3
print(mock()) # 返回 3
# 设置方法的返回值
mock.method.return_value = 3
mock.method() # 返回 3
# 在构造函数中设置返回值
mock = Mock(return_value=3)
mock() # 返回 3
# 设置属性
mock = Mock()
mock.x = 3
mock.x # 返回 3

返回随着输入变化-side_effect

side_effect也算一个属性,当你不满足指定一个常量返回时,就会期望用上它。

# 1.将side_effect设置为一个异常类
mock = Mock(side_effect=Exception('Boom!'))
mock() # 调用时就会抛出异常

from requests.exceptions import Timeout
requests = Mock()
requests.get.side_effect = Timeout # 模拟API超时
with self.assertRaises(Timeout):
    # get_holidays函数里面调用了requests.get,那么将会捕获到Timeout异常
    get_holidays()
# 将会抛出异常

# 2.将side_effect设置为一个迭代器(场景:mock对象被多次调用,每次返回值不一样)
mock = MagicMock(side_effect=[4, 5, 6])
mock()
4
mock()
5
mock()
6

# 3.将side_effect设置为一个函数(场景:返回值由输入参数决定)
vals = {(1, 2): 1, (2, 3): 2}
def side_effect(*args):
    return vals[args]

mock = MagicMock(side_effect=side_effect)
mock(1, 2)
1
mock(2, 3)
2

mock一个类

def some_function():
    instance = module.Foo()
    return instance.method()
# 模拟Foo这个类
with patch('module.Foo') as mock:
    # 此处的“mock”就是一个类,mock.return_value代表该类返回的实例(instance)
    instance = mock.return_value
    # 模拟实例方法的返回(此处的方法名就叫method)
    instance.method.return_value = 'the result'
    # 函数中对Foo的调用就会使用模拟类
    result = some_function()
    assert result == 'the result'

模拟一个对象(object)的方法(method)

patch

patch可能是使用最多的方法,它的使用场景:

1.模拟一个类的属性

2.模拟一个模块的属性

如果我们测试的函数在同一个文件中可以不使用patch,patch主要用在测试代码和主代码分离的情况下。

有3种装饰器可用:

# patch的第一个参数是一个string,形式:package.module.Class.attribute,以此指定要模拟的属性,第二个参数是可选的,第一个参数里面的属性将被替换为该值。
@patch('package.module.attribute', sentinel.attribute)
# 例子一,传有第二个参数:
mock = MagicMock(return_value=sentinel.file_handle)
with patch('builtins.open', mock):
    handle = open('filename', 'r')
# 例子二,不传第二个参数:
# 不传第二个参数时,mock对象会被传入在函数的参数里,如下,并且注意顺序:
class MyTest(unittest.TestCase):
    @patch('package.module.ClassName1')
    @patch('package.module.ClassName2')
    def test_something(self, MockClass2, MockClass1):
        self.assertIs(package.module.ClassName1, MockClass1)
        self.assertIs(package.module.ClassName2, MockClass2)
# 例子三,使用as,将会获得一个引用
with patch('ProductionClass.method') as mock_method:
    mock_method.return_value = None
    real = ProductionClass()
    real.method(1, 2, 3)

# 例子四,将path装饰在类上,作用于每个测试函数
@patch('mymodule.SomeClass')
class MyTest(unittest.TestCase):

    def test_one(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)

    def test_two(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)
		# 装饰在类上只针对test开头的函数,此处不是test开头,不传递MockSomeClass参数
    def not_a_test(self):
        return 'something'

# 例子五,另一种在整个类中模拟的办法
class MyTest(unittest.TestCase):
    def setUp(self):
        patcher = patch('mymodule.foo')
        self.addCleanup(patcher.stop)
        self.mock_foo = patcher.start()

    def test_foo(self):
        self.assertIs(mymodule.foo, self.mock_foo)


# patch的类在同一个文件中
# 此处需要使用__main__,代表当前模块
@patch('__main__.SomeClass')


# patch.object第一个参数是一个对象,第二个参数是该对象的属性名称,第三个是可选的,第二个参数里面的属性将被替换为该值。
# 场景:只模拟部分属性而非整个对象
@patch.object(SomeClass, 'attribute', sentinel.attribute)

@patch.dict()

如下为例子,供参考、copy:

# 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

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

class TestCalendar(unittest.TestCase):
    # patch装饰在函数上如果函数里面会调用my_calendar下的requests函数,就会被mock掉
    @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()

mock可以直接使用装饰器,也可以使用上下文管理器,为什么使用上下文管理器?一般原因有如下两个,可自行判断要不要用:

1.只想针对部分代码,而不是整个测试函数

2.patch装饰器已经很多了,装饰器太多影响可读性

patch路径应该是什么?

where to patch?并不是要去引用某个函数本身所在的位置,而是要看这个函数在哪里使用的,如果在使用了的地方有import,那么就应该是那个地方的路径。
举例:

一个文件中(这个文件路径:package2.m2.py):

from package1.m1 import fun1
def fun2():
  fun1()

在另一个测试文件中:

class JustTest(TestCase):
  @patch('package2.m2.fun1') # 这才是正确的路径,而不是package1.m1.fun1
  def test_fun2(self, mock_fun1):
    mock_fun1.return_value = 3

参考

python官方示例

How to: Unit testing in Django with mocking and patching

What is Mocking?

https://realpython.com/python-mock-library/

posted @ 2020-07-26 21:27  ssh_alitheia  阅读(1101)  评论(0编辑  收藏  举报