【Pytest】Pytest相关面试题

这是一份从入门到深入的 Pytest 面试笔记,共 72 道题,覆盖基础使用、fixture、参数化、Mock、插件机制、并行与异步,以及接口自动化和 CI 的实际项目场景。

热度说明: 以下星级是结合测试开发、自动化测试岗位常见考点做的经验判断,不是招聘平台统计或实时流行度排名,不同公司和职级会有差异。

标记 经验热度 复习建议
★★★★★ 高频核心 应能脱稿解释,并现场写基本代码
★★★★☆ 常见追问 应能解释设计取舍和常见故障
★★★☆☆ 进阶区分项 适合中高级测试开发岗位
★★☆☆☆ 专项深入 按项目经验和岗位要求准备

阅读顺序: 第一轮掌握所有五星题;第二轮补齐四星题;第三轮练习插件机制、异步、并行和项目设计题。回答建议遵循“结论 → 原理 → 例子 → 边界与取舍”。

版本与示例约定: 核心内容采用 Pytest 常用稳定 API,整理时参考官方 stable 文档。插件行为会随版本变化,项目应固定 Python、Pytest 和插件版本。普通代码块是独立示例;标明文件名的多文件示例要按目录配套使用。项目场景中的业务客户端、数据库封装等属于设计说明,不代表 Pytest 自带能力。

一、入门:发现、执行与断言(1—10)

1. Pytest 是什么?为什么在自动化测试中使用它?

热度:★★★★★|难度:入门

参考答案: Pytest 是 Python 测试框架,负责发现测试、组织依赖、运行用例和汇总结果。它既能执行函数级单元测试,也能组织接口、数据库、浏览器等集成测试。

核心优势是普通函数即可成为测试用例,assert 失败时能显示表达式信息;fixture 可以复用准备与清理逻辑;参数化可以让同一测试逻辑覆盖多组输入;插件生态可以扩展报告、并行和覆盖率。

面试加分点: Pytest 本身不是 HTTP 客户端、浏览器驱动或压测工具。接口测试通常还需要 requests/httpx,浏览器测试需要相应驱动,Pytest 负责测试组织与执行。不要把项目中用到的所有能力都说成 Pytest 内置。

2. Pytest 与 unittest 有什么区别?能混用吗?

热度:★★★★★|难度:入门

对比项 Pytest unittest
来源 通常额外安装 Python 标准库
用例组织 测试函数、普通测试类 通常继承 TestCase
断言 普通 assert assertEqual 等断言方法
前后置 fixture,兼容部分 xunit 风格 setUp、tearDown 等
数据驱动 内置参数化 可用 subTest 或其他方案

参考答案: Pytest 能运行大多数 unittest 测试,因此迁移时可以先换执行器,再逐步改用 fixture 和参数化,不必一次重写。

常见追问: unittest.TestCase 测试方法不能像普通 Pytest 函数那样直接接收 fixture 参数,也不能直接使用 Pytest 参数化机制。部分 fixture 能通过 autouse 或 usefixtures 做环境准备,但不能由此推断两种测试模型完全一致。

3. Pytest 默认如何发现用例?为什么有些测试没有被收集?

热度:★★★★★|难度:入门

参考答案: 对普通 Python 测试,默认收集 test_*.py、*_test.py 文件;收集 test 开头的测试函数,以及 Test 开头且没有自定义 __init__ 的测试类中的测试方法。测试类通常不需要继承某个框架基类。

1
2
3
4
# test_math.py
class TestMath:
    def test_add(self):
        assert 1 + 1 == 2

未收集时检查文件名、函数名、类名、执行路径、testpaths、忽略规则,以及模块导入是否报错。先运行 python -m pytest --collect-only -q,把“没发现”和“发现后执行失败”区分开。

易错点: 测试类中写 __init__ 初始化数据可能导致不被正常收集,推荐用 fixture。参见测试发现与项目组织。

4. 常用执行命令有哪些?-k 和 -m 的区别是什么?

热度:★★★★★|难度:入门

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
python -m pytest tests/                         # 执行目录
python -m pytest tests/test_user.py             # 执行文件
python -m pytest tests/test_user.py::TestUser::test_login  # 指定测试项
python -m pytest -k 'login and not invalid'     # 名称等关键字表达式
python -m pytest -m 'smoke and not slow'        # marker 表达式
python -m pytest -q                            # 精简输出
python -m pytest -v --tb=short                  # 详细名称、简短回溯
python -m pytest -x                            # 首次失败后停止
python -m pytest --maxfail=3                    # 达到失败阈值后停止
python -m pytest --lf                          # 使用缓存重跑上次失败项
python -m pytest --ff                          # 上次失败项优先,随后运行其他项

参考答案: -k 根据名称和相关关键字筛选,-m 根据标记筛选。团队应使用 marker 表达测试分类,用测试项 ID 精确定位失败;只用模糊名称筛选容易选错或漏选。

追问: --lf 依赖本地缓存,不能替代一次完整回归,也不能假设全新 CI 环境拥有上次执行记录。参见执行命令。

5. pytest 与 python -m pytest 有什么区别?rootdir 会修复导入吗?

热度:★★★★☆|难度:基础

参考答案: python -m pytest 使用指定 Python 解释器中的 Pytest,适合避免虚拟环境不一致;还会遵循 python -m 的行为,将当前目录加入 sys.path。直接执行 pytest 时,要确认命令属于哪个环境。

rootdir 用于确定配置、测试项 ID 等上下文,不会自动把根目录加入 sys.path。导入失败时,应检查项目包是否安装、目录布局和导入模式,不能仅靠设置 --rootdir 解决。

项目建议: 使用正确的打包配置,在虚拟环境中安装项目,必要时采用 src/ 布局;减少到处修改 sys.path 的临时方案。参见导入机制。

6. Pytest 为什么能展示详细断言信息?辅助模块也一样吗?

热度:★★★★☆|难度:进阶

参考答案: Pytest 会对符合条件的测试模块进行断言重写,在导入执行前处理断言表达式,因此失败时可以展示左右值等信息。它不是简单地捕获 AssertionError 后猜测表达式。

辅助断言模块不一定自动获得相同的重写行为。如果插件或共享包需要重写,应在目标模块首次导入前调用 pytest.register_assert_rewrite("package.assertions")。

1
2
3
4
5
6
# 测试模块中的简单断言

def test_response():
    response = {"code": 0, "data": {"name": "Alice"}}
    assert response["code"] == 0
    assert response["data"]["name"] == "Alice"

易错点: 不要先导入辅助模块,再注册重写;也不要把生产代码中不可省略的参数校验建立在普通 assert 上。参见断言与重写。

7. 浮点数、集合和无序结果应如何断言?

热度:★★★★☆|难度:基础

参考答案: 浮点数使用 pytest.approx 设置容差;无序数据根据业务语义选择集合、排序或 Counter,不能一律转换成集合。

1
2
3
4
5
6
7
from collections import Counter
import pytest


def test_values():
    assert 0.1 + 0.2 == pytest.approx(0.3, abs=1e-12)
    assert Counter(["a", "a", "b"]) == Counter(["b", "a", "a"])

集合会忽略重复次数。如果接口返回的条目不能重复,转成集合后可能掩盖缺陷;Counter 更适合顺序无关但次数有意义的结果。金额规则优先使用明确的最小货币单位或 Decimal,容差不能替代业务精度设计。

8. 如何验证异常?raises 和 xfail 能互相替代吗?

热度:★★★★★|难度:基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pytest


def parse_age(value):
    age = int(value)
    if age < 0:
        raise ValueError("age must be non-negative")
    return age


def test_negative_age():
    with pytest.raises(ValueError, match="non-negative") as exc_info:
        parse_age("-1")
    assert exc_info.type is ValueError

参考答案: pytest.raises 表示“抛出异常就是正确行为”,满足条件时测试通过;xfail 表示“当前测试预计不能满足正常预期”。两者语义不同。

match 是正则匹配;如果需要匹配用户输入中的特殊符号,应考虑 re.escape。raises 默认接受指定异常的子类,只有业务要求精确类型时才额外比较 exc_info.type。

易错点: 将过多准备逻辑放进 raises 块,可能把准备阶段的异常误判为目标异常;异常抛出后的同一块内代码也不会继续执行。

9. 测试有哪些阶段?FAILED 和 ERROR 如何区分?

热度:★★★★★|难度:基础

参考答案: 一个正常运行的测试项通常经历 setup → call → teardown。setup 准备 fixture 等环境,call 执行测试函数,teardown 清理已准备的资源。

测试函数中的断言或其他未处理异常通常显示为 FAILED;fixture 初始化或清理阶段出错通常显示为 ERROR。一个测试的调用阶段失败,清理阶段也可能再次报错,因此不能只记录单一“最终布尔值”。

追问: 若 setup 失败,call 通常不执行,但已经成功准备的 fixture 仍需清理。收集阶段的导入错误属于更早的问题,不能误认为测试函数已经运行。

10. 如何调试失败?退出码有哪些?

热度:★★★★☆|难度:基础

参考答案: 先使用测试项 ID 单独复现,再查看回溯、输入数据、日志和环境。可用 --pdb 在失败时进入调试器,--trace 在测试开始处进入调试;并行调试时先切回单进程,减少干扰。

退出码 含义
0 测试执行成功
1 存在测试失败
2 执行被中断
3 Pytest 内部错误
4 命令行使用错误
5 没有收集到测试

CI 应检查退出码,不能只判断有没有报告文件。退出码 5 需要结合任务意图处理,不能无条件变成成功。参见退出码定义。

二、核心:fixture 与资源生命周期(11—24)

11. fixture 是什么?为什么不直接调用工具函数?

热度:★★★★★|难度:基础

参考答案: fixture 提供测试依赖,并由框架管理创建、复用和释放。测试函数通过参数名称声明需求,Pytest 按依赖关系准备对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import pytest


@pytest.fixture
def user():
    return {"name": "Alice", "role": "admin"}


def test_role(user):
    assert user["role"] == "admin"

普通工具函数适合纯计算、格式转换等显式调用逻辑;fixture 适合客户端、账号、数据库连接等有生命周期的依赖。不要把所有工具函数都包装成 fixture,否则依赖图会难以理解。

追问: fixture 的名称是依赖解析入口,不应在测试中直接调用被装饰的 fixture 函数。需要复用其业务逻辑时,将逻辑拆到普通函数,再由 fixture 调用。

12. fixture 有哪些 scope?扩大 scope 一定更好吗?

热度:★★★★★|难度:基础

scope 通常的共享范围
function 每个测试调用,默认值
class 测试类
module 测试模块
package 测试包
session 当前测试会话

参考答案: scope 决定缓存和生命周期边界。低作用域隔离性好,高作用域减少重复初始化成本。选择时同时考虑资源价格、可变状态和并发安全。

例如 HTTP 连接池可能适合会话复用,而订单、购物车和可变用户资料一般需要每个用例独立创建。参数化 fixture 即使在同一 scope 内也可能发生多次初始化,不能简化为“session 就一定只创建一个对象”。参见fixture 使用指南。

13. fixture 为什么按名称注入?一次测试中请求两次会创建两个对象吗?

热度:★★★★☆|难度:进阶

参考答案: Pytest 根据参数名称解析 fixture,并缓存当前有效实例。一个测试函数和它依赖的另一个 fixture 同时请求同一个 fixture,通常会拿到同一次创建的对象,而不是各创建一份。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import pytest


@pytest.fixture
def items():
    return []


@pytest.fixture
def prepared(items):
    items.append("ready")
    return items


def test_shared_in_one_test(items, prepared):
    assert items is prepared
    assert items == ["ready"]

易错点: 同一用例内共享是依赖机制的一部分;跨用例是否共享取决于作用域和参数化。需要同一用例创建多个独立对象时,应使用工厂 fixture,而不是误以为重复声明依赖就会重新创建。

14. fixture 执行顺序由什么决定?

热度:★★★★★|难度:进阶

参考答案: 主要看作用域、显式依赖和 autouse。高作用域资源先准备;依赖项先于依赖它的 fixture;同一作用域中适用的自动 fixture 会优先执行,其依赖也随之提前。

假设 client → token → account,准备顺序为 account → token → client,正常清理时按相反方向释放。若两个 fixture 之间没有约束,不能把当前观察到的排列当成稳定接口。

面试陷阱: 函数定义位置、fixture 名字和测试参数的左右顺序,都不应承担强制执行顺序的职责。如果 B 必须等 A,直接让 B 声明依赖 A。参见fixture 可见性与执行顺序。

15. return、yield 和 request.addfinalizer 如何选择?

热度:★★★★★|难度:进阶

参考答案: return 提供值;yield 在提供值的同时,把清理写在后半段;addfinalizer 可在运行过程中动态登记清理回调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pytest


@pytest.fixture
def resource_file(tmp_path, request):
    path = tmp_path / "resource.txt"
    path.write_text("ready", encoding="utf-8")
    # 资源创建成功后立即登记,之后的初始化即使失败也会触发回调。
    request.addfinalizer(lambda: path.unlink(missing_ok=True))
    return path


def test_resource_file(resource_file):
    assert resource_file.read_text(encoding="utf-8") == "ready"

yield 的后置代码只有在执行到 yield 后才会进入清理流程;已登记的 finalizer 即使后面的 fixture 初始化失败,仍会被调用。通常优先选择清晰的 yield fixture;动态多资源场景可用 finalizer 或 ExitStack。

易错点: 先登记清理、后创建资源,可能在资源根本不存在时运行错误的清理。登记时机和清理幂等性都要设计。

16. fixture 在 yield 前失败,如何避免部分资源泄漏?

热度:★★★★☆|难度:进阶

参考答案: 将多个资源拆为彼此依赖的小 fixture,或每创建成功一个资源就登记对应清理。不要把“创建账号、开连接、启动任务”全部堆在一个 yield 前面,再假设末尾清理永远会执行。

例如账号 fixture 创建成功并 yield,客户端 fixture 再依赖账号。即使客户端初始化失败,账号 fixture 已进入可清理状态,仍能删除账号。

项目边界: 进程被强杀、机器断电等情况下,Python 清理逻辑可能无法执行。对外部资源还要有测试运行标签、过期时间和定期回收机制。框架内清理保证不等于跨故障的绝对保证。

17. conftest.py 的作用和可见性是什么?

热度:★★★★★|难度:基础

参考答案: conftest.py 是目录范围的本地插件入口,可定义 fixture 和 hook,Pytest 自动发现并加载。测试通常无需手动导入。

1
2
3
4
5
6
7
8
project/
├── pytest.ini
└── tests/
    ├── conftest.py
    ├── test_login.py
    └── orders/
        ├── conftest.py
        └── test_order.py

orders 下的测试可以使用上层 fixture,并通过更近的同名定义覆盖它;其他兄弟目录不会因为文件存在,就自动获得 orders 内的 fixture。

追问: 根目录放全项目通用资源,业务目录放局部资源。避免巨型 conftest.py,也不要把它当普通工具模块相互导入,否则容易产生导入歧义和循环依赖。

18. autouse=True、usefixtures 和参数注入的区别?

热度:★★★★★|难度:基础

参考答案: 参数注入既声明依赖又拿到返回值;usefixtures 显式要求某测试或测试类执行 fixture,但不提供变量;autouse=True 在可见且适用的范围自动执行。

例如统一设置时区、禁止真实外部请求,可以考虑自动 fixture;订单数据、登录身份等业务依赖更适合显式声明,让读者一眼看出用例需要什么。

易错点: usefixtures 不会将返回值放进测试的局部变量;在 fixture 函数上写 usefixtures 也不能替代真正的参数依赖。自动 fixture 不是“整个仓库无条件执行”,它仍受可见性和作用域影响。

19. 为什么出现 ScopeMismatch?怎么改?

热度:★★★★☆|难度:进阶

参考答案: 长生命周期 fixture 请求了短生命周期 fixture。例如 session 资源依赖 function 资源,但后者每个测试变化一次,无法成为整个会话稳定的依赖。

处理方式有两类:缩短请求方的生命周期;或者拆分高作用域的基础资源与低作用域的业务状态。例如 session 数据库引擎提供连接能力,function 事务或测试账号负责用例隔离。

易错点: 为了消除异常,把所有 fixture 都改成 session,可能把一个明显的生命周期错误变成难排查的数据污染。依赖方向应该与资源寿命一致。

20. 什么是工厂 fixture?适合什么场景?

热度:★★★★☆|难度:进阶

参考答案: fixture 返回一个可调用对象,测试可以按需多次调用,创建不同参数的资源。适合同一用例需要多个账号、订单或文件,并希望集中清理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest


@pytest.fixture
def make_file(tmp_path):
    created = []

    def factory(content):
        path = tmp_path / f"item_{len(created)}.txt"
        path.write_text(content, encoding="utf-8")
        created.append(path)
        return path

    yield factory
    for path in reversed(created):
        path.unlink(missing_ok=True)


def test_two_files(make_file):
    left = make_file("left")
    right = make_file("right")
    assert left != right
    assert left.read_text(encoding="utf-8") == "left"

追问: 资源创建成功后再加入清理清单;删除按逆序进行,适合存在父子依赖的资源。外部清理还需处理部分删除失败。

21. fixture 能参数化吗?与测试参数化有什么区别?

热度:★★★★☆|难度:进阶

参考答案: fixture 可通过 params 生成多种依赖配置,值由 request.param 获取;所有请求它的测试都会覆盖这些配置。测试参数化则更适合某一个测试的输入输出组合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import pytest


@pytest.fixture(params=["admin", "viewer"], ids=["admin", "viewer"])
def role(request):
    return request.param


def test_known_role(role):
    assert role in {"admin", "viewer"}

数据库类型、序列化协议等多用例共同维度适合 fixture 参数化;单个校验函数的边界值适合测试参数化。两者组合可能放大用例数,设计前先估算执行成本。

22. 如何覆盖上层 fixture?动态获取 fixture 有什么风险?

热度:★★★☆☆|难度:进阶

参考答案: 在更近的测试模块或目录 conftest.py 中定义同名 fixture,可以覆盖上层定义。扩展上层同名 fixture 时,可以通过参数请求上层版本,再附加局部配置。

request.getfixturevalue(name) 能按运行时名称获取 fixture,适合依赖名确实由运行条件决定的场景。但会隐藏静态依赖、增加定位难度,也不能突破 scope 约束。

项目建议: 优先使用显式参数依赖;覆盖时保持语义一致,不要在某个目录把“只读客户端”同名替换成有写入副作用的对象,破坏调用方预期。

23. fixture 与 setup_method、teardown_method 怎么选?

热度:★★★★☆|难度:基础

参考答案: Pytest 支持 xunit 风格的前后置方法,简单类内准备可以使用;fixture 更适合跨模块复用、按依赖组合、控制 scope 和参数化。

例如多个测试类都需要账号、客户端和订单时,用 fixture 可以只声明当前测试所需的资源,不必依赖深层继承。类属性式共享状态则更容易形成隐性耦合。

易错点: 不要在两个体系中重复创建和删除同一资源。渐进迁移时先明确每个资源归谁管理,再替换前后置机制,防止出现双重清理或先后顺序假设。

24. 如何查看 fixture 来源、作用域和实际准备顺序?

热度:★★★☆☆|难度:进阶

1
2
3
4
python -m pytest --fixtures -v
python -m pytest tests/test_user.py::test_login --fixtures-per-test
python -m pytest tests/test_user.py::test_login --setup-show
python -m pytest tests/test_user.py::test_login --setup-plan

参考答案: --fixtures 查看可用 fixture,--fixtures-per-test 看某测试依赖,--setup-show 随执行显示准备清理,--setup-plan 展示计划而不实际运行测试和 fixture。

定位问题时优先确认同名 fixture 是否被局部覆盖,再看参数化和 scope。盲目加 print 容易被输出捕获干扰,也难以还原完整依赖关系。

三、熟练使用:参数化、标记和配置(25—34)

25. 如何使用 parametrize?为什么比循环断言更适合数据驱动?

热度:★★★★★|难度:基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import pytest


@pytest.mark.parametrize(
    "left,right,expected",
    [(1, 2, 3), (0, 0, 0), (-1, 1, 0)],
    ids=["positive", "zero", "opposite"],
)
def test_add(left, right, expected):
    assert left + right == expected

参考答案: 每组数据生成独立测试项,可以独立统计、定位和重跑。普通循环中,第一条断言失败通常就终止整个测试,后续数据没有执行,报告粒度也更粗。

ids 应使用短而稳定的业务描述,不要放密码、token 或个人信息,因为 ID 会出现在日志和报告中。对于连续业务步骤,则不一定应拆成独立参数项,要看每项是否能独立准备环境。

26. 多个参数化装饰器如何组合?如何控制组合爆炸?

热度:★★★★☆|难度:进阶

参考答案: 叠加参数化通常生成笛卡尔积,例如 3 个地区、4 种角色和 5 种支付方式会得到 60 组测试;若再叠加 fixture 参数化,数量继续增长。

全组合适合维度少且交互风险高的情况。实际项目可以按业务约束剔除无效组合,对核心路径做完整组合,其余采用代表值、边界值或成对组合策略,并说明剩余风险。

易错点: 用例数量多不等于覆盖有效。大量永远被 skip 的组合既拖慢收集,又让报告难以阅读。参见参数化行为与组合。

27. 什么是间接参数化?indirect=True 到底改变了什么?

热度:★★★★★|难度:进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pytest


@pytest.fixture
def account(request):
    return {"role": request.param, "enabled": True}


@pytest.mark.parametrize(
    "account,expected", [("admin", True), ("viewer", True)],
    indirect=["account"],
)
def test_account(account, expected):
    assert account["enabled"] is expected

参考答案: 普通参数化把值直接传给测试;间接参数化先把值传给同名 fixture 的 request.param,再把 fixture 结果注入测试。这里测试拿到字典,fixture 拿到角色字符串。

它适合把轻量配置转换为昂贵资源,初始化在 setup 阶段进行。indirect=True 表示相关参数都走 fixture;只需要部分参数时用名称列表。

常见错误: 误把 fixture 对象本身放进参数列表,期待框架自动解析;或者忘记定义对应 fixture。

28. pytest.param 能做什么?可变参数有什么坑?

热度:★★★★☆|难度:进阶

参考答案: pytest.param 可以给单组数据设置 ID 和标记,例如只给已知缺陷对应的数据加 xfail,而不是把整组用例都标记为预期失败。

1
2
3
4
5
6
7
8
9
import pytest


@pytest.mark.parametrize("value,expected", [
    pytest.param(2, 4, id="normal"),
    pytest.param(3, 9, id="another"),
])
def test_square(value, expected):
    assert value * value == expected

参数中的列表、字典不会自动深拷贝。如果不同测试项引用同一个可变对象,并在运行时修改它,可能造成相互污染。可以使用不可变输入,或在 fixture 中依据配置重新构建对象;不要把运行结果写回原始测试数据。

29. 用例数据放在 YAML、JSON、CSV 还是代码里?

热度:★★★★☆|难度:项目应用

参考答案: 小型输入输出表直接写在代码里更直观,也便于代码审查;跨工具共享的结构化数据可以选择 JSON/YAML;扁平大量数据可选择 CSV。选择标准是可读性、类型表达和维护成本,不是“文件越多越像框架”。

外部数据要进行结构校验、稳定排序和唯一 ID 校验;文件路径应基于模块或明确配置定位,不能依赖偶然的工作目录。读取 YAML 时使用安全加载方式。

追问: 把逻辑表达式、循环和任意代码执行都塞进 YAML,会形成一套难调试的自制语言。数据文件负责输入与预期,业务行为留在可测试的代码中。

30. skip、skipif、xfail、XPASS 有什么区别?

热度:★★★★★|难度:基础

机制 含义 典型使用
skip 不运行该测试 当前暂不适用
skipif 满足条件时跳过 系统或依赖条件不满足
xfail 正常执行但预计失败 已知缺陷,仍需跟踪
XPASS 预期失败的测试意外通过 缺陷可能已修复或预期失效

参考答案: xfail 通常仍执行测试,strict=True 会把意外通过变成失败,促使团队更新状态;特殊情况下可用 run=False 禁止执行,但需说明原因。

1
2
3
4
5
6
7
import sys
import pytest


@pytest.mark.skipif(sys.platform == "win32", reason="仅验证 POSIX 行为")
def test_posix_platform():
    assert sys.platform != "win32"

项目要求: 跳过和预期失败应附原因、缺陷编号或到期条件,不应成为长期掩盖失败的入口。参见跳过与预期失败。

31. 如何注册 marker?为什么建议严格校验?

热度:★★★★★|难度:基础

1
2
3
4
5
6
7
8
# pytest.ini
[pytest]
testpaths = tests
addopts = -ra --strict-markers
markers =
    smoke: 核心冒烟测试
    integration: 集成测试
    slow: 执行较慢的测试

参考答案: 自定义 marker 是测试分类和附加元数据,例如冒烟、集成或慢测试。注册后有统一说明,--strict-markers 能把拼写错误等未注册标记转成错误,防止筛选无声失效。

@pytest.mark.smoke 本身不会改变业务逻辑,实际选择由 -m smoke 或团队 hook 决定。不要把 marker 当权限控制机制,危险环境还需要执行前的硬校验。

32. 配置文件如何选择?多个文件会合并吗?

热度:★★★★☆|难度:进阶

参考答案: 常见配置入口包括 pytest.ini 和 pyproject.toml;配置格式与发现优先级要以项目固定版本为准。团队应选定一个权威入口,避免同目录多份配置相互遮蔽。

Pytest 的配置发现不是把找到的所有文件逐层合并。运行时应查看启动头部的 rootdir、配置文件和插件,必要时用 -c 指定配置。

例如 pyproject.toml 的常用兼容写法是 [tool.pytest.ini_options];pytest.ini 则是 [pytest],不能直接复制节名。执行选项还可能来自环境变量和命令行,因此排查时要记录完整调用环境。参见官方配置说明。

33. 如何新增 --env 参数,并避免误连环境?

热度:★★★★☆|难度:进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# conftest.py
import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--env", action="store", default="dev",
        choices=["dev", "staging"], help="目标测试环境",
    )


@pytest.fixture(scope="session")
def base_url(pytestconfig):
    urls = {"dev": "https://dev.example.com", "staging": "https://staging.example.com"}
    return urls[pytestconfig.getoption("--env")]

参考答案: 用 pytest_addoption 注册命令行选项,通过配置对象或 fixture 读取;环境到地址的映射集中管理,测试用例只依赖客户端。

示例是配置演示,不向这些地址发请求。真实项目中环境参数还应受允许列表、凭据和账号权限约束;不能因为传了一个字符串,就允许任意目标写入。机密信息不放在 URL 或测试项 ID 中。

34. 动态生成测试用例如何实现?与普通参数化有什么区别?

热度:★★★☆☆|难度:深入

参考答案: 可在收集阶段使用 pytest_generate_tests(metafunc),根据测试所需参数和配置调用 metafunc.parametrize。适合多模块复用同一动态数据来源,而不是到处复制装饰器。

1
2
3
4
5
# conftest.py

def pytest_generate_tests(metafunc):
    if "region" in metafunc.fixturenames:
        metafunc.parametrize("region", ["cn", "sg"], ids=["cn", "sg"])
1
2
3
4
# test_region.py

def test_region(region):
    assert region in {"cn", "sg"}

收集阶段应快速、确定、无业务写入。不能一边从不稳定远程数据源实时取数,一边假设并行 worker 一定生成相同测试项。昂贵的资源初始化应推迟到 fixture setup,必要时使用间接参数化。

四、深入测试设计:Mock、输出和可测性(35—44)

35. monkeypatch、unittest.mock、mocker 有什么区别?

热度:★★★★★|难度:基础

参考答案: monkeypatch 是 Pytest 内置 fixture,主要负责临时替换属性、字典项、环境变量、工作目录等,并在作用域结束后恢复;unittest.mock 是标准库,提供 Mock 对象、行为模拟和调用断言;mocker 来自额外的 pytest-mock 插件,便于管理 Mock 生命周期。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os


def region():
    return os.getenv("APP_REGION", "local")


def test_region(monkeypatch):
    monkeypatch.setenv("APP_REGION", "cn")
    assert region() == "cn"

可以让 monkeypatch 安装一个 Mock 对象,由 Mock 记录交互。不要把“替换目标”和“检查调用”看成完全相同的职责。参见monkeypatch 官方指南。

36. 为什么 patch 没生效?应该 patch 哪个位置?

热度:★★★★★|难度:进阶

参考答案: 替换被测代码实际查找对象的位置,而不是机械地替换对象最初定义的位置。

1
2
3
4
# gateway.py

def send(message):
    raise RuntimeError("示例中的真实网关禁止调用")
1
2
3
4
5
6
# service.py
from gateway import send


def notify_user(user):
    return send(f"hello {user}")
1
2
3
4
5
6
7
8
9
# test_service.py
from unittest.mock import patch
import service


def test_notify():
    with patch("service.send", return_value="ok") as send_mock:
        assert service.notify_user("Alice") == "ok"
        send_mock.assert_called_once_with("hello Alice")

service 已绑定导入的 send 名称,只替换 gateway.send 不会自动重新绑定它。若代码通过 gateway.send() 访问,则围绕该访问路径替换。

37. return_value、side_effect、spec、autospec 怎么用?

热度:★★★★☆|难度:进阶

参考答案: return_value 设置调用结果;side_effect 可表示异常、连续结果或动态计算;spec 限制模拟对象可用的属性,spec_set 还限制设置不存在的属性;autospec 能基于真实对象约束调用签名,减少“接口已经改了,Mock 仍然通过”的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from unittest.mock import Mock, create_autospec
import pytest


def fetch(user_id, *, timeout):
    return {"id": user_id}


def test_mock_contract():
    client = create_autospec(fetch, return_value={"id": 1})
    assert client(1, timeout=2) == {"id": 1}
    with pytest.raises(TypeError):
        client(1)

    flaky = Mock(side_effect=[TimeoutError("temporary"), "ok"])
    with pytest.raises(TimeoutError):
        flaky()
    assert flaky() == "ok"

autospec 不能保证业务协议正确,也不能取代真实接口或契约测试。参见标准库 Mock 文档。

38. 断言 Mock 调用时常见错误有哪些?

热度:★★★★☆|难度:进阶

参考答案: assert_called_with 检查最近一次调用,不代表只调用了一次;assert_called_once_with 同时检查次数和参数。验证顺序时可比较调用列表,但不要对无关实现细节过度约束。

另一个隐蔽问题是可变参数:Mock 记录的可能是对象引用,调用后对象被修改,再检查历史参数时看到的已经不是原始内容。需要保留快照时,可以在副作用函数中复制必要字段。

项目取舍: 应验证业务上有意义的交互,例如扣款金额与幂等键,而不是把每个内部辅助函数调用次数都写死。后者会让正常重构引发大量无意义失败。

39. Mock、Stub、Fake、Spy 有什么区别?Mock 越多越好吗?

热度:★★★★☆|难度:进阶

参考答案: 常用术语中,Stub 提供预设结果,Mock 强调交互期望,Fake 是可工作的简化实现,Spy 记录或观察调用。不同库命名可能不完全一致,面试重点是说清替身的用途。

例如内存版仓库可作为 Fake,固定返回用户资料的是 Stub,验证支付网关只被正确调用一次可使用 Mock,包装真实函数观察调用可使用 Spy。

取舍: 外部网络、昂贵资源和难触发异常适合替换;核心业务逻辑应尽量真实执行。若把被测逻辑本身全部 Mock 掉,测试很可能只证明“预设返回值会被返回”。

40. 如何断言标准输出?capsys 与 capfd 有什么区别?

热度:★★★☆☆|难度:进阶

1
2
3
4
5
6

def test_output(capsys):
    print("created")
    captured = capsys.readouterr()
    assert captured.out == "created\n"
    assert captured.err == ""

参考答案: capsys 捕获 Python 的 sys.stdout、sys.stderr;capfd 在文件描述符层捕获,适合还包含底层库或继承相关描述符的子进程输出。二进制输出有相应的 binary fixture。

-s 用于关闭普通输出捕获,适合本地观察;它不是日志断言方法,也不能保证 xdist 下每个 worker 的输出像串行一样实时展示。读取捕获结果后,应留意后续输出属于新的捕获段。参见输出捕获说明。

41. 如何使用 caplog 验证日志?

热度:★★★☆☆|难度:进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import logging


def test_warning_log(caplog):
    logger = logging.getLogger("payment")
    with caplog.at_level(logging.WARNING, logger="payment"):
        logger.warning("payment declined")
    assert any(
        r.name == "payment" and r.levelno == logging.WARNING
        and r.getMessage() == "payment declined"
        for r in caplog.records
    )

参考答案: 根据 logger、级别和关键字段断言,比只在整段文本中查一个词更准确。日志格式中的时间、线程号等常变化内容一般不应写死。

追问: 若被测代码重新配置 root logger 并移除了捕获 handler,caplog 可能失效;应检查日志配置,而不是先认定系统没有产生日志。测试日志也应脱敏,避免把认证头直接写进报告。

42. tmp_path 与 tmp_path_factory 如何选择?

热度:★★★★☆|难度:基础

参考答案: tmp_path 提供当前测试独立的临时目录,返回 pathlib.Path;tmp_path_factory 可以在高作用域 fixture 中创建目录,适合会话级准备昂贵静态文件。

1
2
3
4
5

def test_file(tmp_path):
    path = tmp_path / "result.txt"
    path.write_text("ok", encoding="utf-8")
    assert path.read_text(encoding="utf-8") == "ok"

不要让所有测试写同一个固定路径,也不要假设当前工作目录可写。Pytest 临时目录可能保留部分历史运行以便排查,因此临时目录并不意味着测试结束后立即安全擦除,敏感数据仍需专门管理。

43. 如何测试时间、随机数和超时逻辑?

热度:★★★★☆|难度:项目应用

参考答案: 优先让业务函数接收时钟、随机源或等待器,以便测试显式控制。持续时间使用单调时钟,业务日期使用带明确定义时区的时间。

测试重试逻辑时注入可控制的等待函数,验证重试上限和退避计算,无需真的等待几十秒。随机测试记录 seed 和输入,以便复现;固定 seed 也不能保证来自多个独立随机源的行为全部一致。

易错点: 只 patch 某个模块中的 datetime,不代表所有库和进程的时间都被冻结。系统时间、单调时间与事件循环时间也不是同一概念,测试应明确控制哪一个。

44. 单元测试、集成测试和端到端测试如何分工?

热度:★★★★★|难度:项目应用

参考答案: 单元测试关注局部规则和边界,尽量快速确定;集成测试验证数据库、HTTP 协议或消息系统等真实协作;端到端测试覆盖少量关键业务路径。

例如价格计算可以用大量单元参数化测试,订单持久化用真实测试数据库验证,购买链路用少量端到端用例检查系统是否协同工作。

面试加分点: 分类不应只按是否用 Pytest 或是否发 HTTP 请求决定,而要看验证边界。端到端用例通常更慢、故障面更大,因此应通过分层减少重复覆盖;同时保留真实集成测试,防止 Mock 与真实服务协议漂移。

五、框架进阶:hook、插件、并行与异步(45—54)

45. hook 与 fixture 有什么区别?常用 hook 有哪些?

热度:★★★★☆|难度:深入

参考答案: fixture 管理测试需要的依赖与资源;hook 在框架生命周期事件发生时参与处理,可影响配置、收集、报告等。Pytest 基于 pluggy 组织插件和 hook 实现。

hook 常见用途
pytest_addoption 注册命令行选项
pytest_configure 初始化插件配置、登记元数据
pytest_generate_tests 收集时生成参数组合
pytest_collection_modifyitems 调整收集后的测试项或标记
pytest_runtest_makereport 处理 setup/call/teardown 阶段报告
pytest_sessionfinish 会话结束时进行汇总

追问: 用 fixture 准备登录状态,用 hook 统一处理失败报告。不要在 hook 中隐藏业务测试前置条件,否则依赖会变得难以追踪。hook 的参数和返回值要遵循官方定义,不能任意返回自己想要的对象。

46. 如何通过 pytest_runtest_makereport 获取真实执行结果?

热度:★★★★☆|难度:深入

参考答案: 该 hook 会针对不同阶段生成报告。包装器可以先 yield 让其他实现产生结果,再读取报告,关注 when、outcome 和错误信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# conftest.py
import pytest


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()
    if report.when == "call" and report.failed:
        report.sections.append(("diagnostic", f"failed node: {item.nodeid}"))

示例使用传统 hookwrapper=True 写法;新式 wrapper=True 的返回和异常处理协议不同,不能混用模板。

项目追问: 失败截图通常应在浏览器销毁前采集,保存路径要唯一;采集失败应作为附加诊断保留,不应覆盖原始业务异常。只检查 when == "call" 会漏掉 setup 和 teardown 错误,完整质量报告必须覆盖三个阶段。参见hook 编写规则。

47. 多个插件实现同一个 hook 时,顺序和返回值如何处理?

热度:★★★☆☆|难度:深入

参考答案: hook 有普通实现和包装器实现,tryfirst、trylast 可表达相对优先级,但不应将插件安装顺序当成业务契约。包装器包围普通实现,其 yield 前后处于不同执行阶段。

部分 hook 采用 firstresult 语义,拿到第一个非 None 结果后不再继续查找普通结果;另一些收集多个实现结果。具体行为必须看 hook specification。

易错点: 不该有返回值的 hook 随意返回数据,或在一般 hook 中抛异常,可能破坏整个测试会话。可恢复的诊断错误应局部处理,真正的配置错误则应尽早、清晰地暴露。

48. 如何把公共能力抽成插件?插件本身如何测试?

热度:★★★☆☆|难度:深入

参考答案: 项目内可从 conftest.py 起步;跨项目复用时封装成安装包,通过 pytest11 entry point 注册插件,并管理依赖版本。用 --trace-config 检查加载情况,避免同一插件重复注册。

插件测试不能只测内部函数,还应验证它对真实收集、执行和报告的影响。Pytest 提供 pytester 辅助创建临时测试工程并运行测试,需要先启用 pytester 插件。

1
2
3
4
5
6
7
8
# test_plugin.py:独立插件测试示例
pytest_plugins = ["pytester"]


def test_generated_case(pytester):
    pytester.makepyfile("def test_ok():\n    assert 1 + 1 == 2\n")
    result = pytester.runpytest("-q")
    result.assert_outcomes(passed=1)

测试重点: 正常通过、setup 失败、call 失败、清理失败、用户配置错误,以及需要支持的 Pytest/插件版本组合。参见插件编写与测试。

49. xdist 如何实现并行?如何选择调度策略?

热度:★★★★★|难度:进阶

参考答案: pytest-xdist 启动多个工作进程,控制进程分发测试项并汇总结果。它不是让单个测试函数自动多线程执行。安装插件后可运行 python -m pytest -n 4 或 -n auto。

策略 主要特点 适用取舍
load 按负载分发测试项 通用,但可能重复资源初始化
loadscope 按模块或类等作用域分组 减少部分 fixture 重复准备
loadfile 同文件测试放到同一 worker 适合文件级资源复用
loadgroup 按 xdist_group 分组 控制特定测试同 worker
worksteal 在 worker 间调整待执行工作 用例耗时差异较大时可评估

调度策略与插件版本相关。多进程数量还应受下游服务容量约束,不能单纯追求 CPU 核数。参见xdist 调度文档。

50. xdist 下 session fixture 是否全局只执行一次?如何真正只初始化一次?

热度:★★★★★|难度:进阶

参考答案: 不是。每个 worker 都有自己的会话和进程内缓存,同一个 session fixture 可能在多个进程分别执行。普通全局变量也不能自动跨进程共享。

若初始化结果可以序列化,可在单机共享目录中使用进程锁,锁内检查结果是否存在,不存在则初始化并原子写入,其他 worker 读取。若是共享数据库或远程任务,需要更可靠的外部协调、唯一约束或租约。

边界: 本地文件锁通常不能协调不同 CI 机器;“只创建一次”还涉及失败重试和进程崩溃。业务允许时,按 worker 创建独立资源往往比强制全局共享更稳健。参见xdist 会话级初始化说明。

51. 为什么 xdist 提示不同 worker 收集到的用例不一致?

热度:★★★★☆|难度:深入

参考答案: worker 需要收集到一致的测试项及顺序。参数来自无稳定顺序的集合、随机数、实时数据库、按当前时间变化的接口结果时,不同进程可能得到不同列表或 ID。

应将测试输入固定为本次运行的快照,使用稳定排序和稳定 ID;随机数据的生成需要明确种子和边界。资源的随机名称通常可以在 setup 时生成,而不是让它改变收集项。

排查方法: 对比各 worker 的 node ID 列表、数据版本和配置;先单独验证收集过程,再检查调度。不要通过无限重试 worker 来掩盖收集不确定性。

52. 如何测试 async def?pytest-asyncio 的 auto 与 strict 有什么区别?

热度:★★★★☆|难度:进阶

参考答案: Pytest 核心不能独自完成一般异步测试的事件循环执行,需要适配插件。使用 pytest-asyncio 时可以显式标记测试,并用专用装饰器声明异步 fixture。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import asyncio
import pytest
import pytest_asyncio


@pytest_asyncio.fixture
async def payload():
    await asyncio.sleep(0)
    yield {"ready": True}


@pytest.mark.asyncio
async def test_payload(payload):
    await asyncio.sleep(0)
    assert payload["ready"] is True

strict 模式要求相应的异步测试标记及异步 fixture 声明,适合与其他异步插件共存;auto 模式会自动接管 asyncio 测试和相应 fixture。示例采用显式写法,减少对隐含配置的依赖。

易错点: 仅写 async def 并不保证测试被正确等待;异步测试也不意味着所有测试会自动并发执行。参见pytest-asyncio 概念说明。

53. 异步 fixture 的 scope 与 loop_scope 有什么区别?

热度:★★★☆☆|难度:深入

参考答案: fixture scope 管理对象缓存和清理边界,loop scope 管理异步代码使用哪个生命周期的事件循环。二者不是同一个参数。

例如 session 缓存的异步客户端可能绑定创建它的事件循环。若测试在其他循环中继续使用它,可能出现跨循环 Future 错误或 Event loop is closed。需要使客户端、fixture 和使用者的循环安排兼容,而不是只扩大 fixture scope。

pytest-asyncio 的循环配置和旧版本差异较大,当前文档要求 loop scope 不小于 fixture 的缓存 scope。新项目不要照搬旧教程里重写 event_loop fixture 的方案,应依据固定插件版本配置,并确保关闭客户端和取消后台任务。参见异步 fixture 装饰器。

54. 覆盖率、超时、重试和报告分别是谁提供的?

热度:★★★★☆|难度:进阶

能力 常见实现 注意事项
JUnit XML Pytest 内置 --junitxml 适合 CI 结果接入
覆盖率 pytest-cov 与 coverage.py 要正确指定源码范围
并行 pytest-xdist 外部资源隔离
异步 asyncio pytest-asyncio 固定模式与循环配置
Mock fixture pytest-mock 的 mocker 标准库 Mock 本身不依赖此插件
超时 pytest-timeout 等 强制中断可能影响清理
失败重试 pytest-rerunfailures 等 保留首次失败证据
Allure 报告 allure-pytest 与报告工具链 结果采集与报告生成是不同环节

参考答案: 要明确框架本体和插件边界。命令行参数不存在时,先检查插件是否安装在当前环境、版本是否支持及是否成功加载,不要直接归因于用例代码。

版本追问: pytest-cov 7.0 起移除了自身原有的子进程覆盖率支持,需按 coverage.py 的子进程方案配置;不能认为测了父进程就自动覆盖所有子进程。参见pytest-cov 子进程迁移说明。

六、实际项目:架构、数据、稳定性与 CI(55—72)

55. 从零搭建接口自动化项目,如何分层?

热度:★★★★★|难度:项目设计

场景: 有用户、订单、支付等多个模块,需要支持多个测试环境和日常回归。

参考答案: 先明确测试边界、运行环境和核心业务风险,再设计最小可用分层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
project/
├── pyproject.toml       # 依赖与项目配置,固定兼容版本
├── clients/             # HTTP 连接、超时、认证与脱敏日志
├── services/            # 可复用的业务操作
├── tests/
│   ├── conftest.py      # 公共客户端、环境和依赖
│   ├── users/
│   ├── orders/
│   └── payments/
├── testdata/            # 稳定的输入与预期
└── reports/             # 执行产物,不放敏感凭据

请求层负责协议细节,业务层表达创建订单等操作,测试层表达预期和边界,fixture 管理数据生命周期。公共断言可以封装重复规则,但不应把所有业务预期藏在工具内部。

加分点: 先做一条可独立运行、可清理、可报告的核心路径,再扩展;框架价值在于失败可解释、数据可隔离和维护成本可控,而不是目录或装饰器数量。

56. 登录只调用一次,token 过期和多角色怎么办?

热度:★★★★★|难度:项目设计

场景: 大多数用例需要登录,测试执行一小时,token 十分钟过期,还存在管理员与普通用户。

参考答案: 将认证放在专门提供者中,按环境、账号和角色隔离缓存,并记录有效期;客户端使用前检查是否接近过期,按策略刷新。session fixture 可以复用提供者,而不是永久复用一个不会变化的字符串。

刷新并发需要同步,避免多个请求同时刷新导致旧 token 失效。401 也可能代表权限、账号或服务问题,不能一律刷新并重发所有请求。

关键边界: 只有确认操作可安全重放时才自动重试;支付、创建资源等写请求需要幂等保障。注销、改密码等认证测试应使用独立账号,不能破坏共享登录态。xdist 下还需明确每个 worker 独立缓存或使用安全的集中式方案。

57. A 创建订单,B 使用订单,如何避免执行顺序依赖?

热度:★★★★★|难度:项目应用

参考答案: 把订单创建放到 fixture,使用者显式依赖它,保证 B 单独运行也能得到完整环境。如果要覆盖“下单 → 支付 → 退款”的完整状态迁移,可在一个场景测试里组织连续步骤。

独立接口行为测试和完整链路测试可以同时存在,但应明确各自目的。不要依赖文件名排序或排序插件来弥补缺失的前置条件。

追问: 共享一个订单会导致多个用例并发修改状态。默认按用例创建独立订单,只有只读资源才考虑高作用域共享;清理按业务允许的方式删除、取消或标记回收。

58. 如何隔离测试数据,避免并行冲突?

热度:★★★★★|难度:项目设计

参考答案: 为每次运行创建 run ID,再结合 worker ID、用例或随机标识构造唯一资源名;所有可变数据显式归属于本次运行,清理按归属处理,不能按宽泛前缀误删其他任务的数据。

隔离范围不仅是数据库主键,还包括手机号、邮箱、优惠券、库存、Redis key、消息消费者组、文件路径和端口。必要时按 worker 分配账号池或租户。

易错点: worker_id 只在一次运行内区分 worker,不足以区分两条同时执行的 CI 流水线。唯一名称解决冲突,权限、状态和容量配额仍需独立设计。

59. 数据库事务回滚能解决所有数据清理问题吗?

热度:★★★★★|难度:项目设计

参考答案: 同进程集成测试若被测代码和测试共用可控连接,可以通过事务和保存点回滚。但对远程 HTTP 服务发请求时,服务通常使用自己的连接并提交事务,测试侧的 rollback 无法撤销那些操作。

数据库回滚也不能直接撤销消息发布、缓存更新、对象存储写入和第三方请求。应根据测试边界选择事务隔离、独立数据库/租户、业务清理 API 或按运行标签回收。

项目追问: 被测代码自行提交、后台任务切换连接、DDL 或不同数据库的事务行为,都可能破坏预想的隔离。方案必须用真实写入与回滚验证,不能只因为用了 fixture 就认为数据不会泄漏。

60. 删除测试数据失败,怎样既保留原始失败又避免泄漏?

热度:★★★★☆|难度:项目设计

参考答案: 用例失败与清理失败都需要保留,不能用一个 except Exception: pass 把后者吞掉。清理应幂等,按依赖逆序执行,并尽量尝试释放剩余资源,而不是第一项失败后全部放弃。

可以维护清理清单,逐项收集异常,最后按项目支持的 Python 版本聚合报告;同时记录 run ID、资源 ID 和失败原因,交由受控回收机制补偿。

边界: 请求创建资源超时后,服务器可能已经创建成功,但客户端没拿到 ID。设计时要使用幂等键、业务关联号或可查询的测试标签,不能只清理“客户端确认成功”的资源。

61. 用例偶发失败,怎样判断是产品问题还是测试问题?

热度:★★★★★|难度:故障排查

参考答案: 首先保存首次失败的输入、时间线、请求关联号、服务版本和 worker 信息,再尝试最小化复现。按“单独运行是否失败、串行是否失败、特定顺序是否失败、是否集中在某环境”缩小范围。

常见测试原因包括共享状态、固定 sleep、未等待后台任务、时区与时间边界、随机输入不可复现、过严的时间断言;产品并发缺陷或一致性错误同样可能表现为偶发。

处理策略: 使用有限重试帮助收集证据;隔离不稳定用例时指定负责人、原因和期限,并报告隔离数量及首次失败率。不能直接把“重跑通过”解释为产品没有问题。参见官方不稳定测试讨论。

62. 订单异步处理,怎样避免 sleep(10)?

热度:★★★★★|难度:项目应用

参考答案: 使用有截止时间的条件轮询,查询目标资源达到明确终态后断言。固定 sleep 既可能等得太久,也可能在环境变慢时仍然不够。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
from unittest.mock import Mock


def wait_until(fetch, predicate, *, timeout=2.0, interval=0.01):
    if timeout <= 0 or interval <= 0:
        raise ValueError("timeout and interval must be positive")
    deadline = time.monotonic() + timeout
    last = None
    while True:
        last = fetch()
        if predicate(last):
            return last
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError(f"condition not met, last result: {last!r}")
        time.sleep(min(interval, remaining))


def test_wait_for_paid():
    fetch = Mock(side_effect=[{"status": "pending"}, {"status": "paid"}])
    result = wait_until(fetch, lambda row: row["status"] == "paid")
    assert result["status"] == "paid"

关键边界: 这是同步轮询示例,单次 fetch 自身也必须有请求超时;否则外层截止时间不能中断一次永久阻塞的调用。生产项目要处理终态失败、退避和脱敏,异步测试应使用非阻塞等待。不要用宽泛异常捕获把真实错误无限忽略。

63. 请求超时后要不要重试?如何测试幂等性?

热度:★★★★★|难度:项目设计

参考答案: 超时只说明客户端没有按时获得完整响应,不证明服务器没有执行。查询类操作通常更容易安全重试,写操作应依据业务幂等协议处理,不能只按 GET/POST 名称机械判断。

针对创建订单或扣款,测试同一幂等键的重复请求、并发请求、首次响应丢失后的重放、不同参数复用同一键,以及键过期边界。断言不仅看响应,还要核验最终只产生一次预期副作用。

追问: 用 Mock 模拟超时可以覆盖客户端重试策略;要证明服务端幂等,需要真实集成测试和可观测的持久化/业务结果。请求层自动重试不能掩盖重复扣款风险。

64. 第三方支付、短信或不稳定外部服务怎样测试?

热度:★★★★★|难度:项目设计

参考答案: 分层处理:单元测试使用替身覆盖成功、拒绝、限流、超时和格式异常;契约测试验证客户端与协议一致;少量受控沙箱集成测试验证真实联通和关键流程。

设计适配层,让核心业务依赖明确接口,避免所有测试都依赖真实第三方可用性。测试不能调用真实付费或发信能力来验证基础逻辑,项目应有独立凭据和受限目标。

加分点: 替身响应要随协议变化维护,可从脱敏的固定样本构建;服务降级和失败恢复同样需要断言。只验证“返回了成功”不足以覆盖重复回调、签名校验和乱序事件。

65. 串行通过、并行失败,按什么顺序排查?

热度:★★★★★|难度:故障排查

参考答案: 先用 -n 0 确认串行基线,再用两个 worker 复现,记录失败测试、worker、资源名和时间线。按共享账号/数据、文件与端口、服务限流、初始化协调、收集一致性逐层检查。

若只在高并发失败,还要判断是否触发了真实容量或竞态问题,而不只是测试数据冲突。降低并发有助于定位,但不是自动证明测试设计正确。

临时措施与长期修复: 同文件调度或分组可缓解局部共享问题,但同 worker 不等于整个环境串行。长期应隔离资源、消除顺序依赖,并根据下游容量选择并发数。

66. 3000 条用例运行两小时,如何提速又不降低可信度?

热度:★★★★★|难度:项目优化

参考答案: 先测量耗时分布,区分收集、fixture 初始化、测试调用、清理和报告开销。可用 --durations=20 找热点,并记录请求次数和等待时间。

优先消除重复登录和重复准备,减少不必要的端到端重复覆盖,使用条件等待替代保守 sleep;只读昂贵资源可扩大作用域,可变数据保持隔离。之后再评估并行、分片和调度策略。

PR 流水线可执行快速高风险集合,夜间运行完整回归,但必须说明选择依据和漏检风险,不能把没执行的测试计为通过。指标同时看总耗时、首次失败率、资源成本和缺陷检出,避免“提速后报告更绿但覆盖更弱”。

67. 如何接入 CI?如何定位“本地通过、CI 失败”?

热度:★★★★★|难度:项目应用

1
python -m pytest tests/ --junitxml=reports/junit.xml

参考答案: 固定解释器和依赖,准备必要服务与环境配置,执行后无论成功失败都上传报告和脱敏日志,并按退出码设置任务状态。产物上传步骤不能覆盖原始测试失败状态。

排查本地与 CI 的 Python/依赖版本、操作系统、工作目录、包安装、时区、locale、权限、环境变量、网络和并行配置。比较收集列表,使用同一个测试项和输入复现,而不是只比较启动命令。

追问: 干净 CI 环境通常没有本地缓存,且 Linux 路径大小写更严格;还要检查测试是否依赖未提交文件或执行顺序。定位时先追求环境可重建,再考虑增加重试。

68. 覆盖率 90% 为什么还会漏严重缺陷?如何制定门禁?

热度:★★★★☆|难度:项目设计

参考答案: 行覆盖率表示代码执行过,不表示断言有效,也不能保证边界、分支、并发和跨服务协议被验证。一个只调用函数而没有检查结果的测试也可能贡献覆盖率。

可结合分支覆盖、变更代码覆盖、关键规则用例和少量变异测试评估质量;设置分模块门槛,重点关注核心逻辑和新增代码。排除规则需要审查,避免通过排除难测代码美化数字。

1
2
# 需要安装 pytest-cov;your_package 替换为真实源码包
python -m pytest --cov=your_package --cov-branch --cov-report=term-missing

关键边界: 远程接口自动化测的是另一个服务进程,本地客户端的覆盖率不能当成服务端覆盖率。服务端、子进程和分布式执行都需要独立正确的采集与合并方案。

69. 浏览器测试失败截图如何实现?常见坑有哪些?

热度:★★★★☆|难度:项目设计

参考答案: 浏览器由 fixture 创建并负责关闭,执行结果由报告 hook 获取。调用阶段失败时,在浏览器仍可使用的时点采集截图、当前页面、控制台或网络摘要,并将产物关联到测试项与阶段。

路径可由 run ID、worker ID 和安全化的测试项标识组成,避免多个 worker 覆盖同名文件。setup 失败时浏览器可能尚未创建,teardown 失败时也可能已关闭,必须检查可用性。

易错点: 截图失败覆盖了真正的断言异常;只记录 call 阶段而漏掉初始化失败;附件含密码、token 或个人信息。Allure 等报告插件负责附件展示,但不能替代资源生命周期设计。

70. 消息队列、缓存与最终一致性场景如何验证?

热度:★★★★☆|难度:项目设计

参考答案: 用唯一业务关联号跟踪一次操作,验证它最终产生预期业务结果。必要时使用独立 topic、队列、消费者组或命名空间,避免测试消费者抢走其他系统需要的消息。

等待应有截止时间,并区分处理中、成功终态和失败终态。重复投递场景要验证幂等,乱序场景要验证状态迁移约束;不能仅凭消费到一条消息就认定整个业务完成。

清理边界: 删除数据库记录并不代表已经清理在途消息或缓存。可以停止测试专属消费者、按 run ID 隔离数据并设置过期策略,但不能在共享环境里直接清空整个队列或 Redis。

71. 属性测试能补充参数化吗?适合哪些规则?

热度:★★★☆☆|难度:深入

参考答案: 参数化通常使用人工选择的有限示例;属性测试用工具生成输入,验证对一类输入成立的不变量。Hypothesis 等工具还能缩减失败输入,帮助定位最小反例,它不是 Pytest 核心自带的参数生成器。

适合编解码往返、排序结果有序且元素守恒、金额分配总和不变等性质。应先定义有效输入空间,避免测试充满无意义数据;失败时保留最小反例和相关版本。

易错点: 若业务预期本身不明确,随机生成更多输入也无济于事。对涉及远程副作用的测试,还需控制调用量和清理,不能直接把大量生成案例压到共享环境。人工边界例和属性测试应互相补充。

72. 面试官让你讲一个 Pytest 项目,怎样回答得具体可信?

热度:★★★★★|难度:综合表达

参考答案: 按“业务背景 → 原有问题 → 技术选择 → 实施细节 → 验证证据 → 后续限制”组织,用一个真实案例贯穿,而不是罗列插件名。

例如可以围绕“订单用例串行耗时高、并行又出现共享账号污染”展开:先通过耗时和失败日志定位重复认证与共享订单;将认证提供者与用例级订单 fixture 分开;为每次运行和 worker 隔离账号及业务数据;增加幂等清理;最后用独立重跑、并行回归和故障注入验证。

结果应引用真实记录中的耗时、首次失败率、清理失败数或维护成本,没有记录就坦诚说明,不能编造收益数字。最后讲清哪些测试仍需串行、哪些外部依赖还不稳定,以及为什么接受这些取舍。

高频追问准备: 你负责哪部分?为什么选这个 scope?worker 崩溃后谁清理?为什么不统一重试?怎么证明提速没有降低覆盖?这些问题比“用了哪些插件”更能体现实际经验。

七、易错说法纠正表

常见说法 更准确的表述
session fixture 整个系统只执行一次 当前进程的会话生命周期;并行 worker 各有会话
fixture 按测试参数左右顺序执行 按作用域、依赖和自动执行关系解析
yield 后的清理永远执行 必须到达 yield;进程强杀等也可能绕过清理
重试通过就代表没有缺陷 只能说明再次执行通过,首次失败仍需分析
xfail 就是不运行 通常仍执行,只是预期失败
raises 是已知缺陷标记 它验证“抛出异常”这个正常预期
rootdir 会修复 Python 导入 它不自动修改导入搜索路径
用事务就能撤销远程接口写入 测试事务不能覆盖服务独立连接中的提交
-n auto 是 Pytest 内置参数 来自 pytest-xdist 插件
mocker 是内置 fixture 来自 pytest-mock;monkeypatch 才是内置
异步测试天然同时执行 事件循环执行能力与测试并发调度是两件事
覆盖率高就代表断言质量高 覆盖率只反映执行范围,还需检查预期和风险
Mock 原始定义位置总能生效 必须替换被测代码实际查找名称的位置
所有用例都可以直接重试 写入副作用、幂等性和故障证据必须先考虑

八、复习与现场练习清单

阶段 推荐题目 应达到的程度
入门过关 1—5、7—13、17—18、25、30—31 能写测试函数、fixture、参数化和基本断言
中级扎实 14—16、19—24、26—29、35—44 能解释生命周期、数据隔离和 Mock 边界
进阶区分 33—34、45—54 能读懂 hook,解释并行与异步问题
项目表达 55—72 能说清真实约束、方案取舍、验证证据

建议亲手完成以下练习,而不只背答案:

  1. 写一个工厂 fixture,创建多个资源,模拟中途失败并验证已创建资源仍被清理。
  2. 写三种参数化形式:普通、fixture 参数化、间接参数化;检查生成的 node ID。
  3. 用两个模块演示“patch 定义位置失败、patch 查找位置成功”。
  4. 编写一个 hook,同时记录 setup、call、teardown 的结果,分别制造三类失败。
  5. 对同一组测试分别串行、双 worker 执行,观察 session 初始化次数和资源命名。
  6. 写一个有期限的条件等待,并测试成功、失败终态、超时和查询异常。
  7. 用一个自己的实际项目回答第 72 题,准备日志或真实指标支撑取舍。

九、官方资料索引

以下链接用于核对 API 和版本差异;星级与项目设计建议属于本文的经验整理,不是官方的面试排名。

comments powered by Disqus
使用 Hugo 构建
主题 Stack 由 Jimmy 设计