Python 测试与 CI 实战:从 pytest 到 GitHub Actions 的关键决策点

2026-05-26 30 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

写 Python 项目时,"跑一下没报错"是最常见的验证方式。但项目一旦多人协作、持续迭代,手动验证就不可靠了——测试框架、Mock 策略、代码质量工具、CI 流水线,每一层都影响交付节奏。这篇文章把 unittest、pytest、mock、代码质量工具和 GitHub Actions 串联起来,给出可直接落地的配置和代码。

unittest 还是 pytest:选框架的务实判断

Python 标准库自带 unittest,零依赖就能跑,适合小型脚本或不想引入第三方依赖的场景。但 pytest 在实际项目中优势明显:

  • 断言更自然:直接写 assert result == expected,不需要 self.assertEqual,失败时 pytest 自动展开变量值。
  • fixture 机制:替代 unittest 的 setUp/tearDown,fixture 有明确的作用域(function / class / module / session),依赖注入比继承更干净。
  • 参数化测试:一行 @pytest.mark.parametrize 覆盖多组输入,不用写循环或多个方法。

一个典型的 pytest 参数化示例:

import pytest

def add(a, b):
    return a + b

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, -50, 50),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

运行命令:

pytest test_add.py -v

选择建议:新项目直接用 pytest。如果团队已有大量 unittest 用例,pytest 可以无缝运行它们(pytest 命令会自动发现 unittest.TestCase),逐步迁移即可。

Mock:隔离外部依赖的核心手段

测试中调用真实数据库、HTTP API 或文件系统,会让测试变慢且不可控。unittest.mock 提供两条路径:

  • patch:临时替换模块中的对象,测试结束自动恢复。
  • MagicMock:手动创建模拟对象,注入到被测函数。

下面是一个隔离 HTTP 请求的完整示例:

from unittest.mock import patch
import requests

def fetch_user_name(user_id):
    resp = requests.get(f"https://api.example.com/users/{user_id}")
    return resp.json()["name"]

@patch("requests.get")
def test_fetch_user_name(mock_get):
    # 配置 mock 返回值
    mock_get.return_value.json.return_value = {"name": "Alice"}
    result = fetch_user_name(42)
    assert result == "Alice"
    # 验证 requests.get 被正确调用
    mock_get.assert_called_once_with("https://api.example.com/users/42")

几个容易踩的坑:

  1. patch 路径要用被测模块中的引用,而非原始模块。比如被测文件 import requests,patch 目标是 "my_module.requests.get",而不是 "requests.get"——否则被测代码拿到的仍是原始 requests.get
  2. 别 mock 自己:只 mock 外部依赖,被测逻辑本身不应被替换,否则测试失去意义。
  3. 过度 mock 会让测试与实现耦合:重构代码时 mock 全部失效。优先考虑用轻量本地服务或内存替代(如用 sqlite 替代远程数据库),只在无法替代时才 mock。

代码质量工具:测试之外的防线

测试覆盖行为正确性,但代码风格、类型安全、复杂度等问题需要额外工具:

工具 作用 推荐配置
ruff 替代 flake8 + isort + pyupgrade,极速 lint + 格式化 ruff check . + ruff format .
mypy 静态类型检查 mypy --strict src/(渐进式可放宽)
pytest-cov 测试覆盖率报告 pytest --cov=src --cov-fail-under=80

在项目中一次性配置:

pip install ruff mypy pytest-cov
ruff check . --fix        # 自动修复可修复的问题
ruff format .              # 格式化
pytest --cov=src           # 跑测试并输出覆盖率
mypy src/                  # 类型检查

关键决策:覆盖率阈值设多少?80% 是常见起点,但核心业务模块建议 90%+,工具/脚本类可以放宽。不要追求 100%——那会导致为覆盖率写无意义测试。

GitHub Actions CI:让上述检查自动执行

本地跑一遍没问题就提交,是最常见的失误来源。CI 的作用是:每次 push 或 PR 自动执行 lint、类型检查和测试,不合格就不合并。

下面是一个可直接使用的 GitHub Actions 工作流文件,保存为 .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install ruff mypy pytest-cov

      - name: Lint with ruff
        run: ruff check .

      - name: Type check with mypy
        run: mypy src/
        continue-on-error: true   # 渐进引入时先不阻断

      - name: Run tests with coverage
        run: pytest --cov=src --cov-fail-under=80

      - name: Upload coverage report
        if: matrix.python-version == '3.12'
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: .coverage

配置要点说明

  • strategy.matrix 同时跑多个 Python 版本,提前发现兼容性问题。
  • mypy 步骤加了 continue-on-error: true——项目刚引入类型检查时,先让 CI 不阻断,逐步修复后再改为严格阻断。
  • 覆盖率阈值 --cov-fail-under=80 直接在 CI 中强制,低于 80% 整个 job 失败。
  • upload-artifact 保存覆盖率数据,可配合 Coverage Badge 或后续分析。

上手清单

把上述内容落地到项目,按这个顺序推进:

  1. 安装 pytest 和 pytest-cov,把现有 unittest 用例用 pytest 跑一遍,确认兼容。
  2. 对外部依赖加 mock,优先用本地替代方案,只在必要时 patch
  3. 引入 ruff,一条命令解决 lint + 格式化,替代多个旧工具。
  4. mypy 渐进接入:先 continue-on-error,每周修一批类型问题,修完再收紧。
  5. 创建 .github/workflows/ci.yml,push 后自动跑全流程。
  6. 设置覆盖率阈值:从 80% 开始,核心模块逐步提高到 90%+。

测试和 CI 不是一次性配置,而是持续调整的防线。每次加新功能时问自己:这条路径有测试覆盖吗?CI 能拦截到吗?回答"是"的时候,交付才算可靠。


相关推荐