# 你离场之后，客户敢不敢改代码，取决于这套测试

> Brian Okken 的 pytest 一书，对 FDE 最有用的是一项技能：写出后来者读得懂、信得过的测试。

原文: https://fdetimes.net/zh/books-courses/brian-okken-pytest-book-review/

项目结束，人走了，集成代码留了下来。几个月后，客户那边的某位工程师可能会打开仓库，改一行代码，然后跑一遍测试。他敢不敢合并，取决于你留下的测试套件是否好读、是否可信。

所以，一本讲测试工具的书依然值得前线部署工程师（FDE）花时间读。Brian Okken 的《Python Testing with pytest》第二版由 Pragmatic Bookshelf 于 2022 年 2 月出版，共 272 页。对 FDE 来说，这本书最大的收获是学会写一套后来者第一次读就能看懂的测试套件。

## 这本书教什么，怎么教？

出版社称这一版是全面重写，内容涵盖 assert、fixture、parametrize、marker 和插件，全书围绕一个小型数据库应用展开。

Okken 把它定位为 pytest 的“最平缓、最完整”的教程，让读者循序渐进地学习。按他的说法，第二版从 7 章扩充到 17 章。parametrize 单独成章，覆盖率、mock 和 CI 的讲解也更深入。

目录中有专门讲 mock 的一章，包括 mock 属性、类和方法，以及相关的辅助插件。另有一章讲 tox 和 CI，包括在 GitHub Actions 上运行 tox。

Okken 想说明的是：有了 fixture，pytest 既能写小测试，也能写复杂的功能测试。对 FDE 而言，这意味着单元测试和集成测试不必用两套工具。

## 观点一：准备失败不等于测试失败

书中最有价值的建议，或许是把准备代码从测试函数体移到 fixture 里，这样准备失败和测试失败就能区分开。设想你要交接一个连接器，它把客户 API 中的订单同步到数据仓库。

如果每个测试都自己建连接、自己建临时表，token 一过期，所有测试都会报 FAILED。接手的人会去映射逻辑里找 bug，而那段逻辑其实完全没问题。

把这些工作移到 fixture 中，pytest 就会在准备阶段报 ERROR，而不是在 assert 处报 FAILED。看一眼报告就知道问题出在环境还是代码。根据 pytest 官方文档，使用 yield fixture 时，所有清理代码都放在 `yield` 之后：

```python
@pytest.fixture(scope="module")
def staging_db():
    conn = connect_staging()   # 准备
    yield conn
    conn.drop_temp_tables()    # 清理
    conn.close()
```

fixture 一章还讲了 scope，以及如何通过 `conftest.py` 共享 fixture。交接时，一份整洁的 `conftest.py` 抵得上一页文档：测试运行所需的一切都集中在一处。

**要点:** 你交出去的测试套件，是唯一一份肯定会有人去运行的文档。

## 观点二：一个测试，大量真实数据

书中把 parametrize 介绍为用一个测试跑多组用例的方法。pytest 文档也确认，`@pytest.mark.parametrize` 可以直接在测试函数上声明多组参数。这一章不止讲装饰器，还讲了 fixture 参数化和 `pytest_generate_tests` hook。

拿上面的连接器来说：客户数据很少是干净的。假设订单日期列有三种不同格式。不必写三个测试函数，写一个就够了：

```python
@pytest.mark.parametrize("raw_date, expected", [
    ("2024-03-01", date(2024, 3, 1)),
    ("01/03/2024", date(2024, 3, 1)),
    ("1 Mar 2024", date(2024, 3, 1)),
])
def test_parse_order_date(raw_date, expected):
    assert parse_order_date(raw_date) == expected
```

pytest 会分别报告三个结果，如果只有第二种格式出错，报告会精确指向那一行。每当现场出现一种新的脏数据，就往列表里加一行。到交接时，这份列表就成了你在这家客户遇到过的所有脏数据的记录。

当同一套逻辑要在两个后端上运行时，比如 staging 和本地副本，参数化 fixture 就派上用场了。测试只写一次，由 fixture 决定在哪个环境上跑。

## fixture 新手的两个陷阱

第一个陷阱是把 `conftest.py` 当成杂物堆。只有一个测试文件用到的 fixture，就该放在那个文件里。如果 `conftest.py` 长达几百行，接手的人得从头读到尾才能弄清哪个测试依赖什么，它也就不再起文档的作用。

第二个陷阱是 scope 选错。上面的 `staging_db` fixture 设成 `scope="module"` 是合理的，因为建连接很慢。但如果一个创建示例数据、且测试会修改这些数据的 fixture 也用同样宽的 scope，前面的测试就会给后面的测试留下残余数据，整个套件是绿是红取决于运行顺序。

有一条简单的经验法则：昂贵且只读的资源（连接、客户端）用宽 scope；测试会写入或修改的内容用 `function` scope，并在 `yield` 之后清理。不清理的话，临时表会在客户的 staging 环境里越积越多，最后得由客户团队来收拾。

## 观点三：不自动运行的测试，迟早会坏

书中还介绍了如何用插件扩展 pytest、接入 CI 系统、配合 tox、mock 和覆盖率使用，以及如何运行现有的 unittest 测试。最后这一点听起来不起眼，却很实用：如果客户仓库里已有用 unittest 写的测试，不必从头重写也能用 pytest 运行。

测试时不允许调用客户的生产环境系统，就需要用 mock。而在 GitHub Actions 上运行 tox，能保证每次有人推送代码时测试套件都会运行，哪怕你早已离开项目。

## 谁该读，按什么顺序读？

如果你已经在写 Python，但测试还只是几个零散的 `assert` 函数，建议按这个顺序读：先读前面讲 assert 的几章，再读 fixture，然后是 parametrize。mock、tox 和 CI 留到手上有真正要交接的仓库时再读。

如果你已经熟练使用 pytest，可以直接看 fixture scope、`conftest.py` 和 `pytest_generate_tests`。会用 pytest 和能搭建一整套测试套件，区别就在这里。

从职业发展看，简历上写“编写了测试”打动不了任何人。换成“交接了一个连接器及其 pytest 测试套件，包含 staging 环境的 fixture，并通过 tox 在 GitHub Actions 上自动运行”就不一样了：它说明你会为后来的人着想。

如果你瞄准的 FDE 岗位描述中提到交接或为客户维护代码，面试时准备好讲这个例子。

这本书出版于 2022 年，但书中的内容并未过时。一套好的测试套件，能让客户团队在你离开后有信心修改代码。

**本周动手试试:**

- 找一个在函数体内自己建连接的集成测试，把建连接的部分移到 conftest.py 中的 yield fixture 里
- 把你在客户那里遇到过的最糟糕的五个脏数据样本，汇总到一个 @pytest.mark.parametrize 测试中
- 为即将交接的仓库写一个运行 tox 的 GitHub Actions 工作流，并把链接写进交接文档

## 信源

- [Python Testing with pytest, Second Edition – The Pragmatic Bookshelf](https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition/)

- [Python Testing with pytest, 2nd Edition – Python Test (Brian Okken)](https://pythontest.com/pytest-book/)

- [How to parametrize fixtures and test functions – pytest documentation](https://docs.pytest.org/en/stable/how-to/parametrize.html)

- [How to use fixtures – pytest documentation](https://docs.pytest.org/en/stable/how-to/fixtures.html)
