1. 项目概述:从“能用”到“会测”的思维转变

最近在社区里看到不少朋友写的Python代码,功能实现得挺漂亮,但一提到测试,要么是随手用几个 print 语句糊弄过去,要么干脆就“相信它没问题”。这让我想起自己刚入门那会儿,也是这么干的,直到有一次因为一个边界条件没处理好,线上服务半夜崩了,才真正意识到自动化测试不是“锦上添花”,而是“雪中送炭”。今天我想聊的,就是如何借助 pytest 这个工具,把测试这件事变得简单、高效,甚至有点乐趣。这个标题里的“简单案例”是关键,我们不搞那些复杂到让人望而却步的测试金字塔理论,就从你手头正在写的那个函数开始,看看怎么用几行代码给它套上一个可靠的“安全网”。

pytest 不是什么新潮玩意儿,但在Python测试领域,它绝对是顶流。为什么是它?简单说就是“约定大于配置”。你不需要像用 unittest 那样写一堆样板代码(比如继承某个类),只要遵循它的命名规则,写普通的Python函数就行。它能自动发现并运行你的测试,输出清晰的结果报告,还支持参数化、夹具(fixture)等高级玩法,让测试代码本身也保持DRY(Don‘t Repeat Yourself)。对于从零开始的朋友,记住一点: pytest 的目标是让你更愿意写测试,而不是更头疼。

2. 核心需求解析:我们到底为什么要写测试?

在动手写任何一行测试代码之前,我们得先统一思想:测试到底为了啥?很多人觉得是为了“证明我的代码没错”。这个想法其实有点被动。我更倾向于认为,写测试是一个 主动设计 的过程,它强迫你在实现功能之前,先想清楚这个函数的输入输出边界、各种异常情况。这本身就是一次高质量的逻辑梳理。

具体来说,一套好的测试(哪怕最开始只有一个简单的 pytest 案例)能满足以下几个核心需求:

2.1 快速反馈,防止回归 这是最直接的价值。你修改了A函数,会不会无意中破坏了B函数的功能?如果没有测试,你可能要手动把整个流程跑一遍才能发现,效率极低。有了自动化测试,每次修改后跑一下测试集,几分钟内就能得到反馈。 pytest -x 参数可以在第一个测试失败时就停止,让你立刻聚焦到问题点。

2.2 提升代码可维护性与可读性 测试用例本身就是一份活的、可执行的文档。一个新同事接手你的代码,他看十行注释可能还是云里雾里,但让他跑一遍测试,看看在各种输入下代码的预期行为,理解起来就快多了。 pytest 生成的报告非常直观,哪个用例过了、哪个挂了、错误信息是什么,一目了然。

2.3 支持安全重构 “这段代码写得有点乱,我想重构一下,但又怕改坏了。”——如果你有过这种顾虑,说明你的项目急需测试覆盖。当你有了一套可靠的测试,重构时就会底气十足。只要重构后的代码能通过所有现有测试,基本就能保证外部行为不变。 pytest 配合 pytest-cov 插件还能生成覆盖率报告,告诉你哪些代码行没有被测试到,重构时心里更有谱。

2.4 简化调试过程 当程序出现bug时,如果没有测试,你往往需要搭建一个复杂的运行环境来复现问题。而如果你有为这个功能写的测试,你可以直接在这个隔离的、简单的测试环境中复现问题,并用 pytest --pdb 参数在失败时自动进入调试器,大大缩短了定位问题的时间。

注意:不要陷入“为了测试而测试”的陷阱。测试的终极目标是提升开发效率和软件质量,而不是追求100%的覆盖率数字。先从最重要的、最核心的业务逻辑开始写测试,尤其是那些包含复杂条件判断和计算的部分。

3. 环境搭建与项目初始化

工欲善其事,必先利其器。搭建一个干净、隔离的测试环境是第一步,这能避免系统全局Python环境带来的包冲突问题。

3.1 创建虚拟环境 我强烈建议为每个项目单独创建虚拟环境。这里以项目目录 my_project 为例:

# 进入项目目录
cd my_project
# 使用venv创建虚拟环境,环境文件夹命名为`.venv`
python -m venv .venv

创建完成后,激活虚拟环境:

  • Windows (CMD/PowerShell): .venv\Scripts\activate
  • macOS/Linux: source .venv/bin/activate

激活后,你的命令行提示符前通常会显示 (.venv) ,表示你已进入该虚拟环境。

3.2 安装pytest 在激活的虚拟环境中,使用pip安装 pytest 。为了确保我们也拥有一个代码格式化工具(让测试代码也保持整洁),我通常会把 black 一起装上。

pip install pytest black

安装完成后,可以验证一下:

pytest --version

这应该会输出 pytest 的版本号,比如 pytest 8.x.x

3.3 项目结构规划 一个清晰的项目结构有助于 pytest 自动发现测试。我推荐的最小化结构如下:

my_project/
├── .venv/                  # 虚拟环境目录(通常被.gitignore忽略)
├── src/                    # 源代码目录
│   └── calculator.py       # 我们的被测模块
├── tests/                  # 测试代码目录
│   ├── __init__.py         # 让pytest将tests识别为包(可空文件)
│   └── test_calculator.py  # 针对calculator.py的测试文件
├── requirements.txt        # 项目依赖列表
└── README.md

关键点在于 tests 目录和测试文件的命名。 pytest 默认会查找当前目录下所有以 test_ 开头的文件,以及在这些文件中所有以 test_ 开头的函数或方法。你也可以在 pyproject.toml pytest.ini 配置文件中自定义查找规则。

实操心得:我习惯在项目根目录放一个 requirements.txt 文件,里面记录所有生产环境和开发环境的依赖。可以用 pip freeze > requirements.txt 生成,但更好的做法是手动维护,只写入直接依赖。测试依赖(如 pytest )可以单独放在 requirements-dev.txt pyproject.toml [tool.pytest.ini_options] 部分。

4. 第一个pytest测试案例:从“计算器”开始

理论说再多,不如动手写一个。我们假设正在开发一个简单的计算器模块,里面有一个 add 函数。下面我们一步步为它创建测试。

4.1 编写被测试代码 首先,在 src/calculator.py 中写入我们的业务代码:

def add(a: float, b: float) -> float:
    """返回两个数的和。"""
    return a + b


def divide(a: float, b: float) -> float:
    """返回a除以b的结果。"""
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

这个模块很简单,一个加法函数,一个除法函数(包含了异常处理)。

4.2 编写第一个测试用例 tests/test_calculator.py 中,我们开始编写测试:

from src.calculator import add, divide


def test_add_positive_numbers():
    """测试两个正数相加。"""
    result = add(3, 5)
    assert result == 8


def test_add_negative_numbers():
    """测试两个负数相加。"""
    result = add(-3, -5)
    assert result == -8


def test_add_mixed_numbers():
    """测试正数与负数相加。"""
    result = add(10, -4)
    assert result == 6

看,测试代码就是这么直白。导入要测的函数,然后定义一个以 test_ 开头的函数,在里面调用被测函数,并用 assert 语句来验证结果是否符合预期。 pytest 会捕获 assert 后面的表达式,如果结果为 False ,则测试失败。

4.3 运行测试并解读结果 在项目根目录下(确保虚拟环境已激活),直接运行:

pytest

pytest 会自动发现并运行 tests/ 目录下的所有测试。你会看到类似下面的输出:

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-8.0.0, pluggy-1.0.0
rootdir: /path/to/my_project
collected 3 items

tests/test_calculator.py ...                                              [100%]

============================== 3 passed in 0.02s ===============================

绿色的点和 3 passed 表示三个测试全部通过。这就是最简单的 pytest 工作流。

4.4 测试异常情况 一个好的测试不仅要测“正常路径”,还要测“异常路径”。比如我们的 divide 函数,当除数为0时应抛出 ValueError pytest 提供了 pytest.raises 上下文管理器来测试这类情况。

test_calculator.py 中增加:

import pytest


def test_divide_normal():
    """测试正常除法。"""
    result = divide(10, 2)
    assert result == 5


def test_divide_by_zero():
    """测试除数为零时应抛出ValueError。"""
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    # 还可以进一步断言异常信息
    assert str(exc_info.value) == "除数不能为零"

这里, pytest.raises(ValueError) 断言其代码块内的语句会抛出一个 ValueError 异常。如果没抛出,或者抛出的异常类型不对,测试就会失败。 exc_info 对象包含了捕获到的异常信息,我们可以用它来做更精确的断言。

注意事项: assert 是测试的灵魂,但断言内容要尽可能精确。比如 assert result == 8 就比 assert result 要好,因为后者只要 result 不是 None False 就会通过,可能掩盖逻辑错误。对于浮点数计算,由于精度问题,直接使用 == 比较可能失败,应使用 pytest.approx assert result == pytest.approx(0.3)

5. 使用Fixture管理测试依赖与状态

如果每个测试函数都需要先创建一个数据库连接、或者初始化一个复杂的对象,代码会变得重复且难以维护。 pytest fixture 就是用来解决这个问题的。你可以把 fixture 看作是一个“测试脚手架”,它提供测试所需的数据、状态或资源。

5.1 一个简单的Fixture案例 假设我们的计算器需要一个“记忆”功能,每次运算后累加结果。我们可以创建一个 calculator fixture来提供这个带记忆功能的计算器实例。

首先,修改 src/calculator.py ,增加一个类:

class MemoryCalculator:
    def __init__(self):
        self.memory = 0

    def add(self, value: float) -> float:
        self.memory += value
        return self.memory

    def reset(self):
        self.memory = 0

然后,在 tests/conftest.py 文件中定义fixture( conftest.py pytest 的一个特殊文件,其中定义的fixture可以被同一目录及子目录下的所有测试文件使用):

import pytest
from src.calculator import MemoryCalculator


@pytest.fixture
def calculator():
    """提供一个已初始化的MemoryCalculator实例,每个测试函数独立一份。"""
    calc = MemoryCalculator()
    yield calc  # 这是测试函数真正使用的对象
    calc.reset()  # 测试函数执行完毕后,会回到这里执行清理工作

5.2 在测试中使用Fixture tests/test_calculator.py 中,我们可以这样使用这个fixture:

def test_memory_calculator_add(calculator):  # 将fixture名称作为参数传入
    """测试记忆计算器的加法累加功能。"""
    assert calculator.add(5) == 5
    assert calculator.add(3) == 8  # 5 + 3
    assert calculator.memory == 8


def test_memory_calculator_independence(calculator):
    """验证fixture的独立性:每个测试获得全新的计算器。"""
    # 这个测试中的calculator是全新的,memory为0
    assert calculator.memory == 0
    calculator.add(10)
    assert calculator.memory == 10

当你运行测试时, pytest 会:

  1. 发现 test_memory_calculator_add 函数需要 calculator 参数。
  2. 查找名为 calculator 的fixture并执行它。
  3. yield 语句返回的 calc 对象传递给测试函数。
  4. 测试函数执行完毕。
  5. 回到fixture中,执行 yield 之后的清理代码( calc.reset() )。

5.3 Fixture的作用域 上面的 calculator fixture默认是 function 作用域,即每个测试函数都会调用一次。 pytest fixture还支持其他作用域:

  • session : 一次测试运行只执行一次(所有测试共用)。
  • module : 每个测试文件执行一次。
  • class : 每个测试类执行一次。
  • function : (默认)每个测试函数执行一次。

例如,一个连接数据库的fixture,如果每次测试都连接断开一次太耗时,可以设置为 module session 级别:

@pytest.fixture(scope="module")
def db_connection():
    conn = create_db_connection()
    yield conn
    conn.close()

实操心得:谨慎使用 session module 级别的fixture,特别是它们有状态时。因为测试之间可能会相互影响,破坏测试的独立性。一个原则是:除非初始化成本极高(如启动一个Docker容器),否则优先使用默认的 function 作用域。

6. 参数化测试:用一份代码覆盖多组数据

我们经常需要用多组不同的输入输出数据来测试同一个功能。比如测试加法,我们想测 (1,1,2) , (0,5,5) , (-1,-1,-2) 等多组数据。如果为每组数据都写一个测试函数,代码会非常冗余。 pytest @pytest.mark.parametrize 装饰器就是解决这个问题的神器。

6.1 基础参数化用法 继续用我们的 add 函数举例:

import pytest


@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 1, 2),
        (0, 5, 5),
        (-1, -1, -2),
        (2.5, 3.5, 6.0),
        (0, 0, 0),
    ]
)
def test_add_parametrized(a, b, expected):
    """使用参数化测试多组加法数据。"""
    result = add(a, b)
    assert result == expected

@pytest.mark.parametrize 装饰器第一个参数是一个字符串,定义了测试函数参数的名称( "a, b, expected" ),第二个参数是一个列表,里面的每个元组对应一组参数值。 pytest 会为每一组数据单独运行一次 test_add_parametrized 函数,并在报告中清晰展示每一组数据的执行情况。如果其中一组失败,其他组仍会继续执行。

6.2 参数化与异常测试结合 参数化同样可以用于异常测试。例如,测试 divide 函数:

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (10, 2, 5),
        (9, 3, 3),
        (5.5, 2, 2.75),
    ]
)
def test_divide_parametrized_normal(a, b, expected):
    """参数化测试正常除法。"""
    assert divide(a, b) == expected


@pytest.mark.parametrize(
    "a, b, expected_exception, expected_message",
    [
        (10, 0, ValueError, "除数不能为零"),
        (0, 0, ValueError, "除数不能为零"),  # 0除以0在我们逻辑里也报错
    ]
)
def test_divide_parametrized_error(a, b, expected_exception, expected_message):
    """参数化测试除法异常情况。"""
    with pytest.raises(expected_exception) as exc_info:
        divide(a, b)
    assert str(exc_info.value) == expected_message

6.3 为参数化用例添加ID 当参数组合很多时,输出报告可能难以区分是哪组数据失败了。可以通过 ids 参数为每组数据添加一个可读的标签:

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 1, 2),
        (0, 5, 5),
        (-1, -1, -2),
    ],
    ids=["positive_plus_positive", "zero_plus_positive", "negative_plus_negative"]
)
def test_add_with_ids(a, b, expected):
    assert add(a, b) == expected

运行测试时,报告中就会显示这些ID,更容易定位问题。

注意事项:参数化虽然强大,但不要过度使用。如果每组测试数据需要不同的前置条件(fixture)或后置清理,或者测试逻辑本身因数据不同而有很大差异,那么强行参数化会导致测试函数内部充满条件判断,降低可读性。此时,拆分成多个测试函数可能更清晰。

7. Mock与猴子补丁:隔离测试环境

现代应用很少是孤立的,你的函数可能会调用外部API、读写数据库、访问文件系统。在单元测试中,我们不应该真正去调用这些外部依赖,因为它们可能慢、不稳定、有副作用(比如真的往数据库里插了一条记录)。这时,我们需要“模拟”这些外部对象,这就是Mock(模拟)和Monkeypatch(猴子补丁)的用武之地。

7.1 使用unittest.mock(pytest内置支持) Python标准库 unittest.mock 提供了强大的Mock功能, pytest 可以无缝使用。假设我们有一个函数,它会从一个外部服务获取汇率并计算金额:

# src/currency.py
import requests

def get_usd_to_cny_rate():
    """调用外部API获取美元对人民币汇率(模拟)。"""
    # 这里假设调用了一个真实API
    response = requests.get("https://api.example.com/rate/usdcny")
    return response.json()["rate"]

def convert_usd_to_cny(amount_usd):
    rate = get_usd_to_cny_rate()
    return amount_usd * rate

测试 convert_usd_to_cny 时,我们不应该真的发起网络请求。我们可以用 unittest.mock.patch 来替换 get_usd_to_cny_rate 函数,让它返回一个我们设定的固定值。

# tests/test_currency.py
from unittest.mock import patch
from src.currency import convert_usd_to_cny

def test_convert_usd_to_cny():
    """测试货币转换,模拟汇率获取函数。"""
    # 使用patch装饰器,临时将'src.currency.get_usd_to_cny_rate'替换为一个返回6.5的Mock
    with patch('src.currency.get_usd_to_cny_rate', return_value=6.5):
        result = convert_usd_to_cny(100)
        assert result == 650  # 100 * 6.5

patch 装饰器/上下文管理器会将被测模块中指定的对象(函数、类、属性)替换为一个 Mock 对象。 return_value 参数设定了当这个Mock被调用时的返回值。

7.2 使用pytest的monkeypatch fixture pytest 提供了一个内置的 monkeypatch fixture,用于在测试运行时动态地设置、删除或修改属性、字典项、环境变量等。它比 unittest.mock.patch 在某些场景下更轻量。

例如,测试一个读取环境变量的函数:

# src/config.py
import os

def get_api_key():
    return os.environ.get("MY_API_KEY")

# tests/test_config.py
def test_get_api_key(monkeypatch):
    """测试读取环境变量。"""
    # 使用monkeypatch.setenv临时设置环境变量
    monkeypatch.setenv("MY_API_KEY", "test-key-123")
    from src.config import get_api_key
    assert get_api_key() == "test-key-123"

    # 测试环境变量不存在的情况
    monkeypatch.delenv("MY_API_KEY", raising=False)
    assert get_api_key() is None

monkeypatch 的优势在于,它的修改仅在当前测试函数内有效,测试结束后会自动恢复原状,保证了测试间的隔离。

7.3 验证Mock对象的调用 有时我们不仅关心返回值,还关心某个函数是否被以正确的参数调用了。Mock对象可以记录这些信息:

from unittest.mock import Mock, call

def test_mock_call():
    # 创建一个Mock对象
    mock_sender = Mock()
    # 假设我们有一个发送邮件的函数
    def send_notification(user, message, sender):
        sender.send(user.email, message)

    user = Mock(email="test@example.com")
    send_notification(user, "Hello!", mock_sender)

    # 断言send方法被调用了一次,且参数正确
    mock_sender.send.assert_called_once_with("test@example.com", "Hello!")
    # 或者更精细地检查调用历史
    assert mock_sender.send.call_args == call("test@example.com", "Hello!")

实操心得:Mock是单元测试的利器,但要避免“过度Mock”。如果你发现一个测试里Mock了五六个外部依赖,可能意味着被测试的函数职责太多(违反了单一职责原则),耦合度过高。这时应该考虑重构代码,而不是写更复杂的Mock。Mock应该用于隔离真正的“外部”依赖(如网络、数据库),而不是项目内部的其他模块。

8. 测试配置、插件与高级技巧

当项目变大,测试用例成百上千时,一些配置和高级功能就能显著提升效率。

8.1 配置文件pytest.ini 你可以在项目根目录创建一个 pytest.ini 文件来配置 pytest 的默认行为。

[pytest]
# 指定测试文件的查找路径
testpaths = tests
# 指定Python路径,使得能从项目根目录导入模块
pythonpath = .
# 自动发现测试文件的模式
python_files = test_*.py
# 自动发现测试类和函数的模式
python_classes = Test*
python_functions = test_*
# 添加命令行参数的默认值
addopts = -v --tb=short
# 忽略某些目录
norecursedirs = .venv build dist

-v 参数让输出更详细, --tb=short 让错误回溯信息更简洁。你可以通过 pytest --help 查看所有可用选项。

8.2 有用的命令行参数

  • pytest -x : 遇到第一个失败测试时立即停止。
  • pytest --lf pytest --last-failed : 只重新运行上次失败的测试。
  • pytest -k "keyword" : 只运行名称中包含 keyword 的测试(函数名、类名)。
  • pytest -m marker : 只运行被标记了 @pytest.mark.marker 的测试。
  • pytest --cov=src : 生成测试覆盖率报告(需要 pytest-cov 插件)。

8.3 自定义标记 你可以给测试打上标记,用于分类或条件化执行。

import pytest

@pytest.mark.slow
def test_very_slow_integration():
    # 这个测试很慢,可能涉及真实数据库或网络
    ...

@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
    ...

@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+")
def test_using_new_syntax():
    ...

然后可以通过 pytest -m "slow" 只运行慢测试,或者 pytest -m "not slow" 排除慢测试。

8.4 插件生态 pytest 的强大离不开其丰富的插件生态:

  • pytest-cov : 生成测试覆盖率报告。
  • pytest-xdist : 并行运行测试,加快大型测试集速度。
  • pytest-asyncio : 对异步代码(asyncio)提供测试支持。
  • pytest-django / pytest-flask : 对Django或Flask框架的深度集成支持。

安装后,这些插件会自动被 pytest 加载,提供额外的功能和命令行参数。

9. 常见问题与排查技巧实录

在实际使用 pytest 的过程中,你肯定会遇到一些“坑”。下面是我总结的一些常见问题及其解决方法。

9.1 测试文件无法导入被测模块 问题 :运行 pytest 时提示 ModuleNotFoundError: No module named 'src' 原因 pytest 运行时,当前工作目录(通常是项目根目录)可能不在Python的模块搜索路径( sys.path )中。 解决

  1. 推荐 :在项目根目录创建 pytest.ini ,并设置 pythonpath = . (如上文所述)。
  2. tests 目录下创建 conftest.py 文件,并在文件开头手动添加路径:
    import sys
    from pathlib import Path
    sys.path.insert(0, str(Path(__file__).parent.parent))
    
  3. 使用 pip install -e . 以可编辑模式安装你的项目包。

9.2 Fixture作用域导致的测试污染 问题 :一个测试函数修改了 module session 级别fixture返回的对象状态,导致后续测试失败。 排查 :检查fixture的作用域。如果fixture返回的是可变对象(如列表、字典、自定义类实例),并且被多个测试修改,就会产生污染。 解决

  • 将fixture作用域改为 function (默认),确保每个测试获得全新对象。
  • 如果必须用大作用域fixture(如数据库连接),确保在fixture内部返回的是不可变数据或每次返回独立的副本。例如:
    @pytest.fixture(scope="module")
    def shared_data():
        # 返回一个基础数据,测试中如果需要修改,应创建其副本
        return {"key": "initial_value"}
    

9.3 断言失败时信息不清晰 问题 :对于复杂对象, assert a == b 失败时, pytest 只显示 AssertionError ,看不到 a b 的具体差异。 解决

  • 使用 pytest 的内置断言重写,它已经能很好地展示列表、字典等结构的差异。确保你没有使用 python -m pytest 以外的方式运行,或者禁用了断言重写。
  • 对于自定义对象,可以实现 __repr__ 方法,返回一个清晰的字符串表示,这样在断言失败时就能看到更详细的信息。
  • 在断言前,可以使用 print 打印出关键变量(测试结束后记得移除)。

9.4 测试运行太慢 问题 :测试套件执行时间过长,影响开发效率。 排查与解决

  1. 识别慢测试 :使用 pytest --durations=10 找出最慢的10个测试。
  2. 使用 pytest-xdist 并行运行 pip install pytest-xdist ,然后使用 pytest -n auto 根据CPU核心数并行运行测试。
  3. 优化fixture :将 scope function 提升到 module session (在确保无状态污染的前提下),减少重复初始化开销。
  4. Mock外部调用 :将网络请求、数据库查询、文件IO等慢操作用Mock替换。
  5. 分离测试类型 :使用标记(mark)将单元测试(快)和集成测试/端到端测试(慢)分开。日常开发只跑单元测试: pytest -m "not slow"

9.5 临时目录和文件的处理 问题 :测试需要创建临时文件,测试结束后需要清理。 解决 :使用 pytest 提供的 tmp_path fixture。它返回一个 pathlib.Path 对象,指向一个临时目录,该目录在测试结束后会自动清理。

def test_create_file(tmp_path):
    # tmp_path 是一个独立的临时目录路径
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text("Hello, pytest!")
    assert p.read_text() == "Hello, pytest!"
    # 测试结束后,整个tmp_path目录会被自动删除

9.6 测试异步代码 问题 :测试 async def 函数时,直接调用会得到协程对象,而不是执行结果。 解决 :安装 pytest-asyncio 插件,并使用 @pytest.mark.asyncio 标记你的异步测试函数。

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_function():
    result = await some_async_function()
    assert result == "expected"

也可以在 pytest.ini 中配置自动为异步函数添加标记:

[pytest]
asyncio_mode = auto

从写第一个简单的 assert 语句,到熟练运用fixture组织测试资源,用参数化覆盖多种场景,再用Mock隔离外部依赖,这个过程其实就是测试思维不断成熟的过程。测试不再是负担,而成了你代码设计的一部分。我最深的体会是,当你养成为新功能先写测试的习惯后,你写出的代码接口会更清晰,边界条件会更明确,因为你在实现之前就被迫从调用者的角度思考了一遍。 pytest 提供的这一套工具链,让这种“测试先行”或至少“测试同步”的开发模式变得非常顺畅。下次当你写完一个函数,不妨先别急着去调用它,花几分钟为它写两个测试用例,你会发现,这份对代码的“信心”是任何调试输出都无法替代的。

更多推荐