pytest之模拟对象测试

unittest.mock --- 模拟对象库文档链接:

unittest.mock --- 模拟对象库 — Python 3.13.0a0 文档

MagicMock

导入方式:

from unittest.mock import MagicMock

MagicMock类是用于创建具有魔术方法和属性的模拟对象的一种特殊类的模型类

unittest.mock 模块提供了在测试过程中创建模拟对象的功能,以帮助进行单元测试和集成测试等各种类型的测试,MagicMock类是其中一个常用工具,它可以用来创建模拟对象,并通过模拟对象的方法和属性类模拟真实对象的行为

实例:

from unittest.mock import MagicMock

# 创建一个MagicMock对象
mock = MagicMock()
# 配置MagicMock对象的属性和行为
mock.some_method.return_value = 42
mock.some_property = 'hello'
# 使用 MagicMock 对象
result = mock.some_method()
print(result)  # 输出结果为 42

print(mock.some_property)  # 输出结果为 'hello'

这样如果我们想要模仿其他对象的行为,只需要将mock对象传递给被需要测试的代码中,并使用它来替代真实对象

假设有一个需要测试函数my_func()需要使用某个对象some_object的行为,我们可以使用以下方式将mock对象作为some_object参数传递给my_func():

# 定义一个需要测试的函数
def my_func(some_object):
    result = some_object.some_method()
    return result + 1

# 创建一个 MagicMock 对象并配置其行为
mock_object = MagicMock()
mock_object.some_method.return_value = 42

# 调用被测试的函数,并传递模拟对象作为参数
result = my_func(mock_object)

# 断言函数返回值是否符合预期
assert result == 43

在上面的代码中,我们使用MagickMock类创建了一个mock_object对象,我们这个对象模拟了some_object中的some_method属性,这样我们直接可以把我们的模拟对象作为参数传入,他同样具有这个属性替代真实的对象

在这个例子中,我们定义了一个 my_func() 函数,其中使用了一个 some_object 对象的方法。然后,我们创建了一个 MagicMock 对象,并将其作为 some_object 参数传递给 my_func()。由于 mock_object 对象的行为已经被配置为返回 42,因此当 my_func() 函数调用 some_method() 方法时,将会返回 42,而函数的返回值将被增加 1,最终断言返回值是否符合预期

我们也可以使用下面的方法来进行模拟测试

from unittest.mock import MagicMock


class Index(object):
    aaa = 1

    def __init__(self):
        self.aa = 1
        self.bb = 2

    def index(self):
        index_true = True
        return index_true


mock = MagicMock(spec=Index)
print(mock.aaa)  # <MagicMock name='mock.aaa' id='2365689811280'>
mock.aaa = 2
print(mock.aaa)  # 2
mock.index.return_value = False
print(mock.index())  # False

这里我们可以看到我们在产生对象的时候使用了spec这个参数,这个时候我们的mock对象就会完全拥有Index类的方法与属性,但是这时候注意我们只能够对该属性或者方法进行修改,而不能够真正的访问

使用 MagicMock 对象进行属性和方法访问通常用于模拟类的行为,而不是获取真实的属性或方法。在上面的代码示例中,当你使用 mock.aaa 访问 aaa 类属性时,返回的是一个 MagicMock 对象,而不是真实的属性值。

这种行为是为了方便测试中的模拟和断言操作。你可以通过对 MagicMock 对象的属性进行赋值来模拟属性的更改,以满足你的测试需求。比如在示例中的 mock.aaa = 2,将 mock 对象的 aaa 属性值设置为 2

此外MagicMock还提供默认的魔术方法实现,如下图所示

from unittest.mock import MagicMock

class Index:
    def __str__(self):
        return "This is the original __str__ method"

# 创建一个 Index 类的 mock 对象
index_mock = MagicMock(spec=Index)
# 模拟 __str__ 方法的行为
index_mock.__str__.return_value = "This is the mocked __str__ method"

# 测试调用 __str__ 方法
result = str(index_mock)
print(result)  # 输出: This is the mocked __str__ method

我们在上面的代码中使用MagicMock模拟Index类,这个时候我们可以直接将Index类中的__str__方法修改为我们需要的返回值,这样完成测试的目的

MockFixture

导入方式:

import pytest

from pytest_mock import MockFixture

pytest_mock.MockFixture是pytest_mock库中的一个测试夹具,他是基于pytest的插件,提供了在测试中使用模拟对象的功能

使用MockFixture家具,你可以在测试函数中创建和配置模拟对象,并在测试过程中使用它们,它简化了使用模拟对象进行单元测试、集成测试和功能测试的过程

示例:

# 导入必要的库和模块
import pytest
from pytest_mock import MockFixture

# 定义一个需要测试的函数
def add(a, b):
    return a + b

# 编写测试函数,使用 MockFixture 创建和配置模拟对象
def test_add(mocker: MockFixture):
    # 创建模拟对象并配置其行为
    mock_a = mocker.Mock(return_value=2)
    mock_b = mocker.Mock(return_value=3)

    # 调用被测试的函数,并传递模拟对象作为参数
    result = add(mock_a, mock_b)

    # 断言函数的返回值是否符合预期
    assert result == 5

    # 断言模拟对象是否被正确调用
    mock_a.assert_called_once()
    mock_b.assert_called_once()

 mocker.patch方法:

mocker.patch()是pytest_mock库中的一个方法,用于替换python对象的属性或方法,并在测试用例结束时自动清除这些替换操作,其主要作用是在测试过程中控制代码的行为,从而方便测试

使用mocker.path()方法可以实现以下几种模式:

1.替换对象的属性

2.替换对象的方法

3.替换模块的属性或方法

4.使用上下文管理器方式替换对象的属性和方法

实例:

class MyClass:
    def my_method(self):
        return 42

def test_my_function(mocker):
    # 创建一个 MyClass 的实例
    my_instance = MyClass()

    # 使用 mocker.patch() 替换 my_method 的返回值为 10
    mocker.patch.object(my_instance, 'my_method', return_value=10)

    # 断言 my_method 返回值是否被正确替换
    assert my_instance.my_method() == 10

在使用 pytest-mock 插件时,mocker 是一个夹具(fixture),它是 pytest 的内置夹具之一,并由 pytest-mock 提供增强功能。

当你在测试函数的参数列表中添加 mocker 参数时,pytest 会自动识别并注入 mocker 实例,使得你可以在测试函数中使用它。不需要额外的配置或导入

所以我们在上面的示例中直接就可以使用mocker.path方法

我们现在可以看一下源码他是怎么样的

首先我们知道mocker其实是我们的MockFixture类的对象,那么我们去找我们MockFixture类,他的init是怎么实现的

class MockerFixture:
    """
    Fixture that provides the same interface to functions in the mock module,
    ensuring that they are uninstalled at the end of each test.
    """

    def __init__(self, config: Any) -> None:
        self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = []
        self.mock_module = mock_module = get_mock_module(config)
        self.patch = self._Patcher(
            self._patches_and_mocks, mock_module
        )  # type: MockerFixture._Patcher
        # aliases for convenience
        self.Mock = mock_module.Mock
        self.MagicMock = mock_module.MagicMock
        self.NonCallableMock = mock_module.NonCallableMock
        self.NonCallableMagicMock = mock_module.NonCallableMagicMock
        self.PropertyMock = mock_module.PropertyMock
        if hasattr(mock_module, "AsyncMock"):
            self.AsyncMock = mock_module.AsyncMock
        self.call = mock_module.call
        self.ANY = mock_module.ANY
        self.DEFAULT = mock_module.DEFAULT
        self.create_autospec = mock_module.create_autospec
        self.sentinel = mock_module.sentinel
        self.mock_open = mock_module.mock_open
        if hasattr(mock_module, "seal"):
            self.seal = mock_module.seal

这里参数比较多一点,我们找到self.patch方法,可以看到他其实是另一个类_Patcher产生的对象,而且我们直接通过self点,证明该类是在我们MockFixture中,找到该类,大概看一下这个类中的方法

    class _Patcher:
        """
        Object to provide the same interface as mock.patch, mock.patch.object,
        etc. We need this indirection to keep the same API of the mock package.
        """

        DEFAULT = object()

        def __init__(self, patches_and_mocks, mock_module):
            self.__patches_and_mocks = patches_and_mocks
            self.mock_module = mock_module
        def _start_patch(
            self, mock_func: Any, warn_on_mock_enter: bool, *args: Any, **kwargs: Any
        ) -> MockType:
            pass
        def object(
            self,
            target: object,
            attribute: str,
            new: object = DEFAULT,
            spec: Optional[object] = None,
            create: bool = False,
            spec_set: Optional[object] = None,
            autospec: Optional[object] = None,
            new_callable: object = None,
            **kwargs: Any
        ) -> MockType:
            pass
        def context_manager(
            self,
            target: builtins.object,
            attribute: str,
            new: builtins.object = DEFAULT,
            spec: Optional[builtins.object] = None,
            create: bool = False,
            spec_set: Optional[builtins.object] = None,
            autospec: Optional[builtins.object] = None,
            new_callable: builtins.object = None,
            **kwargs: Any
        ) -> MockType:
            pass
        def multiple(
            self,
            target: builtins.object,
            spec: Optional[builtins.object] = None,
            create: bool = False,
            spec_set: Optional[builtins.object] = None,
            autospec: Optional[builtins.object] = None,
            new_callable: Optional[builtins.object] = None,
            **kwargs: Any
        ) -> Dict[str, MockType]:
            pass
        def dict(
            self,
            in_dict: Union[Mapping[Any, Any], str],
            values: Union[Mapping[Any, Any], Iterable[Tuple[Any, Any]]] = (),
            clear: bool = False,
            **kwargs: Any
        ) -> Any:
            pass

我们可以看到该类中大概有以上几个方法,每个方法代表不同的

mocker.patch

mock.patch() 函数可以接受多个参数,最常用的是其第一个位置参数,用于指定被替换对象的路径或目标,这个路径是以字符串形式的写入的,比如我们的路径是aaa/bbb/ccc,那么我们在传入的时候就写为"aaa.bbb.ccc"

其他可选参数如下:

  • new: 指定一个新的函数或者类实例(mock object),代替被替换对象。如果不提供,将默认使用 mock 对象。
  • spec: 指定一个 class,将检查 mock 实例是否是该 class 的实例,否则将抛出 Attribute error。如果不提供,则不会执行类型检查。
  • spec_set: 与 spec 相似,但是当属性在被替换对象中不存在时,将抛出 AttributeError。
  • autospec: 自动从被替换对象中获取参数和返回值的信息。
  • create: 定义是否自动创建要替换对象上的属性或方法。如果为 True,当需要访问缺失的函数或者属性时,它们将被自动创建。
  • attribute: 要模拟或替换的对象的属性名,例如,我们可以使用 'datetime.datetime' 来替换 datetime 模块。
  • return_value: 模拟函数的返回值。
  • side_effect: 模拟函数的“效果”。当调用模拟函数时,它将执行指定的函数或者抛出异常
mocker.patch.dict

该方法将字典对象或字符串作为输入,并可选择提供键值对的映射关系或迭代器来修改字典的内容。除此之外,还可以设置是否清空字典和其他参数

下面是对方法参数的解释:

  • self: 这是一个实例方法,self 表示当前对象实例自身。
  • in_dict: 要修改的字典对象或字典名的字符串表示。
  • values: 用于替换字典项的键值对映射关系或可迭代对象(如元组列表)。
  • clear: 是否在应用新的键值对映射关系前先清空字典。
  • **kwargs: 其他可选参数,将以关键字参数的形式传递给 mock.patch.dict 方法。

最后,该方法调用了 _start_patch() 方法,其中 self.mock_module.patch.dict 是实际调用的 mock.patch.dict 方法。通过 _start_patch() 方法,它将参数传递给 mock.patch.dict 方法,并返回结果

mocker.patch.object

该方法将对象的属性作为输入,可修改该属性值,也可以指定新的属性值。除此之外,还可以设置其他参数,如属性值类型检查、新对象的创建等

下面是对方法参数的解释:

  • self: 这是一个实例方法,self 表示当前对象实例自身。
  • target: 目标对象,它包含要修改的属性,通常是一个实例对象。
  • attribute: 要修改的属性名。
  • new: 可选参数,指定新的属性值,默认值为 DEFAULT,表示不修改属性值。
  • spec: 指定属性值的类或类型,用于检查新属性值是否符合预期的类型。
  • create: 如果指定的属性不存在,是否创建新属性。
  • spec_set: 与 spec 相似,但在属性名不存在时会引发属性错误。
  • autospec: 自动创建类似 spec 的规范,但使用目标对象的属性来获取类型信息。
  • new_callable: 当 new 不是默认值时,这个参数是一个可调用对象,用于生成新的属性值。
  • **kwargs: 其他可选参数,将以关键字参数的形式传递给 mock.patch.object 方法
mocker.patch.multiple

该方法可以同时对目标对象的多个属性进行修改或模拟,除目标对象外,还可以设置属性值的类型检查,新对象的创建等

下面是对方法参数的解释:

  • self: 这是一个实例方法,self 表示当前对象实例自身。
  • target: 目标对象,它包含要修改或模拟的多个属性,通常是一个实例对象。
  • spec: 指定属性值的类或类型,用于检查属性值是否符合预期的类型。
  • create: 如果指定的属性不存在,是否创建新属性。
  • spec_set: 与 spec 相似,但在属性名不存在时会引发属性错误。
  • autospec: 自动创建类似 spec 的规范,但使用目标对象的属性来获取类型信息。
  • new_callable: 这个参数是一个可调用对象,当属性值需要修改时,用于生成新的属性值。
  • **kwargs: 其他可选参数,将以关键字参数的形式传递给 mock.patch.multiple 方法

使用应用

mocker.patch.dict(os.environ)

使用 pytest_mock 库提供的 mocker 对象对 os.environ 字典进行模拟和替换的方式。这个操作的目的是在测试过程中临时修改和控制环境变量。

具体来说,os.environ 是一个字典,它保存了当前进程的环境变量。通过 mocker.patch.dict(os.environ),我们可以用一个模拟的字典来替换 os.environ,从而在测试期间修改环境变量的行为。

使用 mocker.patch.dict(os.environ) 的好处是,在测试代码中我们可以方便地添加、修改或删除环境变量,而不会影响实际的运行环境。这对于测试特定的环境变量配置和路径设置等场景非常有用。

例如,假设我们要测试一个函数,它基于不同的环境变量值执行不同的逻辑。我们可以使用 mocker.patch.dict(os.environ) 来模拟不同的环境变量值,以确保我们能够覆盖所有可能的情况,并验证函数在各种环境变量配置下的行为是否正确。

下面是一个示例:

import os

def my_function():
    if os.environ.get("ENV") == "development":
        # 在开发环境下执行的逻辑
        pass
    else:
        # 在其他环境下执行的逻辑
        pass

def test_my_function(mocker):
    mocker.patch.dict(os.environ, {"ENV": "development"})
    # 模拟设置环境变量 ENV 的值为 "development"

    my_function()

    # 在这里进行相关的断言和验证

 这里我们使用了mocker.path.dict(os.environ)进行环境变量的设置,然后在调用函数my_function()判断环境变量中是否已经设置成功

注意:MockFixture与MagicMock

今天看源码发现一个新奇的地方是昨天所没有看到的如下图所示

import unittest.mock
class MockerFixture:
    def __init__(self, config: Any) -> None:
        self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = []
        self.mock_module = mock_module = get_mock_module(config)
        self.patch = self._Patcher(
            self._patches_and_mocks, mock_module
        )
        self.Mock = mock_module.Mock
        self.MagicMock = mock_module.MagicMock
        self.NonCallableMock = mock_module.NonCallableMock
        self.NonCallableMagicMock = mock_module.NonCallableMagicMock
        self.PropertyMock = mock_module.PropertyMock
        if hasattr(mock_module, "AsyncMock"):
            self.AsyncMock = mock_module.AsyncMock
        self.call = mock_module.call
        self.ANY = mock_module.ANY
        self.DEFAULT = mock_module.DEFAULT
        self.create_autospec = mock_module.create_autospec
        self.sentinel = mock_module.sentinel
        self.mock_open = mock_module.mock_open
        if hasattr(mock_module, "seal"):
            self.seal = mock_module.seal

在MockerFixture类中提供了对 mock 模块中函数的相同接口,并确保在每个测试结束时卸载这些 mock。

该类的构造函数 __init__ 接受一个参数 config,类型为 Any

在初始化过程中,它首先创建了一个空列表 _patches_and_mocks,用于存储要安装和卸载的 mock 对象。然后,调用 get_mock_module(config) 函数,获取与配置相关的 mock 模块对象,并将其保存到 self.mock_module 中。

接下来,创建了一个内部类 _Patcher 的实例 self.patch,它接受 _patches_and_mocks 列表和 mock_module 对象作为参数。该 _Patcher 类用于简化对 mock 模块中的 patch 函数的使用。

接下来,为了方便使用,将 mock_module 中的一些常用类和函数赋值给相应的属性。例如,self.Mockmock_module.Mockself.MagicMockmock_module.MagicMock,等等。还有一些常用的常量,如 self.ANYself.DEFAULT

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值