跳至内容

pytest-mock 教程:Python 初学者模拟(Mock)的入门指南

学习如何使用 pytest-mock:从环境搭建与基础概念到实用的高级技巧。
已更新 2026年10月6日  · 15分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

随着 Python 项目在复杂度和规模上的增长,健全的测试变得愈发重要。测试有助于确保代码按预期运行,并在应用的各个部分保持质量。

当您的应用需要与不同类型的数据库或诸如 OpenAI GPT、Anthropic Claude 等外部 API 交互时,测试这些依赖会变得具有挑战性。这时,模拟(mocking)便能派上用场。

通过模拟,您可以仿真对象、函数和行为,使测试更加独立且可预测。它还能让您聚焦于想要测试的特定函数,将其与会引入复杂性或意外行为的依赖隔离开来。

在本文中,您将学习如何使用 pytest-mock(一款功能强大的 pytest 插件)来高效地在 Python 测试中实现模拟。到本文结束时,您将能将模拟技术加入到测试用例中,使其更加强大。

什么是 pytest-mock?

pytest-mock 是流行的 Python 测试框架 pytest 的一个插件,提供了便捷的模拟能力。它构建于 Python 内置的 unittest 之上,简化了测试过程中的模拟操作。

pytest 框架标志

pytest 框架标志

pytest-mock 提升了可读性,并以更贴合 pytest 的方式轻松在测试中实现模拟。无论您需要模拟单个函数、类方法,还是整个对象,pytest-mock 都能提供有效测试所需的灵活性,而无需额外复杂度。

pytest-mock 具备良好可读性的关键原因之一,是其声明式的测试风格,允许您在测试中直接指定期望的调用与行为。这使得代码更易编写、阅读与维护,并且在不改变外部行为的实现变更下仍然保持稳健。

设置 pytest-mock

为项目创建虚拟环境是确保依赖隔离的最佳实践。如果您尚未设置新环境,可使用以下命令创建一个 conda 环境:

# create a conda environment
conda create --name yourenvname python=3.11
# activate conda environment
conda activate yourenvname

安装 pytest 和 pytest-mock

使用 pip 或 conda 在您的环境中直接安装 pytest 和 pytest-mock:

pip install pytest
pip install pytest-mock

确认安装

安装 pytest 与 pytest-mock 后,运行以下命令验证安装是否成功:

pytest --version

使用 pytest-mock 进行模拟的基础

可以将模拟理解为创建一个“虚拟”的组件版本,用以模仿真实行为。

当您需要测试函数或类与外部组件(如数据库或外部 API)的交互时,它尤为有用。

模拟简介

一个模拟对象(mock object)会仿真真实对象的行为。它常用于单元测试中以隔离组件,测试其行为而无需执行依赖代码。

例如,您可以使用模拟对象来仿真一次数据库调用,而无需真正连接数据库。

当真实对象符合以下情况时,模拟尤其有用:

  • 难以创建或配置。
  • 使用成本高、耗时长(如访问远程数据库)。
  • 具有应在测试中避免的副作用(如发送电子邮件、产生费用)。

使用 mocker 固件(fixture)

pytest-mock 提供了 mocker 固件,使创建和控制模拟对象变得容易。mocker 固件十分强大,并能无缝集成到您的 pytest 测试中。

💡什么是固件(fixture)?

在 Python 中,固件是可复用的组件,用于为测试设置与清理所需资源,如数据库或文件。它们确保一致性、减少重复,并简化测试搭建。

我们来看一个示例:

def fetch_weather_data(api_client):
    response = api_client.get("https://api.weather.com/data")
    if response.status_code == 200:
        return response.json()
    else:
        return None

fetch_weather_data 函数依赖外部天气 API 来获取数据,但在测试期间调用该 API 可能会带来不必要的成本。

为避免这种情况,您可以在测试中使用模拟来仿真 API 的行为,从而在不发起外部调用的前提下完成应用测试。如下所示:

def test_fetch_weather_data(mocker):
    # Create a mock API client
    mock_api_client = mocker.Mock()

    # Mock the response of the API client
    mock_response = mocker.Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"temperature": "20C", "condition": "Sunny"}

    # Set the mock API client to return the mocked response
    mock_api_client.get.return_value = mock_response

    # Call the function with the mock API client
    result = fetch_weather_data(mock_api_client)

    # Assert that the correct data is returned
    assert result == {"temperature": "20C", "condition": "Sunny"}

    mock_api_client.get.assert_called_once_with("https://api.weather.com/data")

在 test_fetch_weather_data() 中,我们使用 mocker 固件创建了一个模拟的 API 客户端 mock_api_client 和一个 mock_response。该模拟响应被配置为返回状态码 200 和包含天气信息的 JSON 数据,以此来仿真真实的 API 调用。

将模拟 API 客户端的 get 方法设置为返回该模拟响应。测试 fetch_weather_data() 时,它与模拟客户端交互,而不会发起实际的 API 调用。

这种做法确保函数按预期工作,同时使测试快速、低成本,并且不依赖外部系统。

末尾的断言语句用于验证返回数据与预期输出一致。

最后一行 mock_api_client.get.assert_called_once_with("https://api.weather.com/data") 会具体检查模拟 API 客户端的 get 方法是否被且仅被调用一次,并传入了正确的参数。这有助于确认函数与 API 的交互方式符合预期,为测试增添了额外的验证层。

pytest-mock 的常见用例

在需要隔离特定函数或组件的单元测试中,模拟非常有用。下面来看一些常见用例:

1. 模拟函数或类方法

当您需要测试一个依赖其他函数或方法的函数,但希望在测试中控制这些依赖的行为时,可以用模拟将这些依赖替换为预定义行为:

# production code
def calculate_discount(price, discount_provider):
    discount = discount_provider.get_discount()
    return price - (price * discount / 100)

# test code
def test_calculate_discount(mocker):
    # Mock the get_discount method
    mock_discount_provider = mocker.Mock()
    mock_discount_provider.get_discount.return_value = 10  # Mocked discount value
    # Call the function with the mocked dependency
    result = calculate_discount(100, mock_discount_provider)
    # Assert the calculated discount is correct
    assert result == 90
    mock_discount_provider.get_discount.assert_called_once()

在该测试中,discount_provider 的 get_discount() 方法被模拟为返回一个预定义值(10%)。这将 calculate_discount() 函数与 discount_provider 的实际实现隔离开来。

2. 模拟与时间相关的代码

在测试涉及时间或延迟的函数时,模拟时间相关方法能避免等待或操纵真实时间:

# production code
import time
def long_running_task():
    time.sleep(5)  # Simulate a long delay
    return "Task Complete"

# test code
def test_long_running_task(mocker):
    # Mock the sleep function in the time module
    mocker.patch("time.sleep", return_value=None)
    # Call the function
    result = long_running_task()
    # Assert the result is correct
    assert result == "Task Complete"

此处通过补丁替换 time.sleep,从而在测试期间绕过实际延迟。这样测试能快速运行,无需等待 5 秒。

3. 模拟对象属性

有时您可能想要模拟对象的属性或特性(property)行为,以便在测试中仿真不同的状态:

# production code
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    @property
    def is_adult(self):
        return self.age >= 18

# test code
def test_user_is_adult(mocker):
    # Create a User object
    user = User(name="Alice", age=17)
    # Mock the is_adult property
    mocker.patch.object(User, "is_adult", new_callable=mocker.PropertyMock, return_value=True)
    # Assert the mocked property value
    assert user.is_adult is True

在该示例中,User 类的 is_adult() 属性被模拟为始终返回 True,不受实际年龄影响。这对于测试依赖对象状态的场景很有用。

使用 pytest-mock 的高级模拟技巧

当您熟悉了基础用法后,pytest-mock 还提供了一些高级能力以处理更复杂的场景。

1. 模拟副作用(side effects)

模拟不止能返回固定值——还可以复现抛出异常或动态改变返回值等行为。这可通过为模拟添加“副作用(side_effect)”来实现:

import pytest

# production code
def process_payment(payment_gateway, amount):
    response = payment_gateway.charge(amount)
    if response == "Success":
        return "Payment processed successfully"
    else:
        raise ValueError("Payment failed")

# test code
def test_process_payment_with_side_effects(mocker):
    # Mock the charge method of the payment gateway
    mock_payment_gateway = mocker.Mock()
    # Add side effects: Success on first call, raise exception on second call
    mock_payment_gateway.charge.side_effect = ["Success", ValueError("Insufficient funds")]
    # Test successful payment
    assert process_payment(mock_payment_gateway, 100) == "Payment processed successfully"
    # Test payment failure
    with pytest.raises(ValueError, match="Insufficient funds"):
        process_payment(mock_payment_gateway, 200)
    # Verify the mock's behavior
    assert mock_payment_gateway.charge.call_count == 2

通过 side_effect 属性,模拟对象可以在后续调用中返回不同值或抛出异常。在此示例中,第一次调用 charge 返回 "Success",第二次则抛出 ValueError。这有助于在单个测试中覆盖多种情景。

2. 监视(spying)函数

监视可用于追踪真实函数的调用方式,包括调用次数和传入参数。当您想确保函数按预期被调用,同时仍执行其原始逻辑时,这尤其有用:

# production code
def log_message(logger, message):
    logger.info(message)
    return f"Logged: {message}"

# test code
def test_log_message_with_spy(mocker):
    # Spy on the info method of the logger
    mock_logger = mocker.Mock()
    spy_info = mocker.spy(mock_logger, "info")
    # Call the function
    result = log_message(mock_logger, "Test message")
    # Assert the returned value
    assert result == "Logged: Test message"
    # Verify the spy behavior
    spy_info.assert_called_once_with("Test message")
    assert spy_info.call_count == 1

spy() 方法会在不改变真实方法行为的前提下,记录其被调用的方式。在这里,我们监视了记录器的 info 方法,确保其以正确的消息被调用,同时仍让该方法按原样执行。

使用 pytest-mock 的最佳实践

模拟非常强大,但若使用不当,可能会给测试带来问题。以下是我建议遵循的一些最佳实践:

1. 避免过度模拟

过多的模拟会导致测试脆弱:即使外部行为未变,只要内部代码改动,测试也可能失败。尽量只模拟必要的部分,并尽可能依赖真实实现。

过度模拟会让测试与代码实现细节紧密耦合,这意味着哪怕是小幅重构也会让测试不必要地失败。相反,应聚焦于仅模拟系统中外部或具有显著副作用的部分。

2. 使用清晰的命名约定

在进行模拟时,为模拟对象使用具有描述性的名称。这能提升测试的可读性与可维护性。例如,不要使用 mock_function() 这类通用名称,而应使用更具描述性的 mock_api_response()。

清晰的命名约定有助于他人理解每个模拟的用途,减少困惑,并让测试套件在长期维护时更加轻松。

3. 保持测试简洁聚焦

每个测试应聚焦于单一行为或场景。这样能简化调试,并保持测试易于理解。聚焦良好的测试更可能在问题出现时提供清晰反馈,从而更易定位与修复问题。

结语

在本文中,我们探讨了使用 pytest-mock 进行模拟的基础,学习了如何借助它改进 Python 测试。从函数与方法的基础模拟,到添加副作用与监视函数等高级技巧,我们都逐一涵盖。

在处理复杂系统或外部依赖时,模拟是创建可靠且易维护测试的关键工具。将 pytest-mock 融入您的项目,能帮助您编写更独立、可预测、且更易维护的测试。

若想继续拓展测试技能,建议查看 Datacamp 上的 Introduction to Testing in Python 免费课程。

常见问题

模拟与测试真实组件有何不同?

模拟会用仿真组件替换真实组件,使测试在不依赖外部系统或真实数据的情况下实现隔离与成本可控。

我可以将 pytest-mock 与其他测试框架一起使用吗?

pytest-mock 专为 pytest 设计并可与之无缝集成,但它构建于 Python 的 unittest.mock 之上,后者也可独立使用。

在 pytest-mock 中使用固件是必须的吗?

尽管像 mocker 这样的固件能提升模拟效率,您仍然可以在 pytest 中直接使用 unittest.mock 而不借助固件,只是集成度会低一些。


Moez Ali's photo
Author
Moez Ali
LinkedIn
Twitter

数据科学家,PyCaret 的创始人兼作者

主题
Python

通过这些课程进一步学习 Python!

课程

Python for Developers 入门

3 小时
185.3K
掌握 Python 编程基础。 无需任何先验知识!
查看详情Right Arrow
开始课程
查看更多Right Arrow