应用上线的代码一旦改错,最典型的问题:改 A 功能,坏了 B 功能。手动测试的痛点:费时、容易漏、没人愿意每次改完都全量点一遍。
自动化测试 = 把"验证行为"变成可重复执行的代码。Flask 的杀手锏是 test_client:不发真实网络请求,直接在内存里模拟一次完整请求,跑完立刻断言响应。
💡 关键直觉:test_client 就像"替身考官"——真实浏览器要起服务器、发请求、收响应;test_client 让 Flask 直接"内部考一遍",快几个数量级。
# test_app.py from app import app def test_home(): client = app.test_client() # 创建测试客户端 resp = client.get('/') # 模拟 GET 请求 assert resp.status_code == 200 # 断言状态码 assert 'Hello' in resp.get_data(as_text=True) # 断言内容
client.get('/') 返回的 resp 对象拥有:
status_code:HTTP 状态码data / get_data(as_text=True):响应体json:若返回 JSON,可直接取字典headers:响应头def test_login_post(): client = app.test_client() resp = client.post('/login', data={ 'username': 'alice', 'password': 'secret123', }) assert resp.status_code == 302 # 登录成功重定向 def test_api_json(): client = app.test_client() resp = client.post('/api/users', json={ 'name': 'Bob', 'email': 'bob@example.com', }) assert resp.status_code == 201 assert resp.json['id'] > 0
pip install pytest
推荐结构:
project/ ├── app/ # 应用包(用工厂函数 create_app) ├── tests/ │ ├── conftest.py # 共享 fixture │ ├── test_views.py │ └── test_api.py └── requirements.txt
# tests/conftest.py import pytest from myapp import create_app from myapp.db import init_db @pytest.fixture def app(): app = create_app('testing') # 测试配置 with app.app_context(): init_db() # 初始化测试库 yield app # 测试结束后的清理可放这里 @pytest.fixture def client(app): return app.test_client() @pytest.fixture def runner(app): return app.test_cli_runner()
测试里直接注入:
def test_home(client): resp = client.get('/') assert resp.status_code == 200 def test_with_app(app): with app.app_context(): # 应用上下文内可访问 current_app/g from flask import current_app assert current_app.config['TESTING'] is True
原则:测试绝不能碰开发/生产数据库。常见做法:
sqlite:////tmp/test.db 或内存库);# config 中的测试配置 class TestingConfig(Config): TESTING = True DATABASE_URL = 'sqlite:///:memory:' # 内存数据库,跑完即弃 WTF_CSRF_ENABLED = False # 测试时关掉 CSRF 简化表单测试
需要"已登录"状态的接口,可以在测试中直接设置 session:
def test_profile_requires_login(client): resp = client.get('/profile') assert resp.status_code == 302 # 未登录被重定向 def test_profile_logged_in(client): with client.session_transaction() as sess: sess['user_id'] = 1 # 直接写入 session resp = client.get('/profile') assert resp.status_code == 200
session_transaction() 让你在测试里直接修改 session,不必真走一遍登录流程。
def test_blueprint_route(client): resp = client.get('/admin/dashboard') assert resp.status_code == 200 def test_404_page(client): resp = client.get('/no-such-page') assert resp.status_code == 404 assert '页面不存在' in resp.get_data(as_text=True)

取舍原则:
| 误区 | 现象 | 正解 |
|---|---|---|
| 测试连上开发数据库 | 测试污染真实数据 | 用 TestingConfig + 独立/内存库 |
| 只测状态码不测内容 | 页面渲染错了测不出来 | 同时断言关键内容片段 |
| fixture 返回 app 后忘 yield | 清理逻辑不执行 | yield app 再写清理 |
| 测试互相依赖 | 单个跑通过、一起跑挂 | 每个测试独立建数据 |
| 忽略 CLI 命令测试 | 部署脚本坏了没人发现 | 用 app.test_cli_runner() 测命令 |
# tests/test_app.py from myapp import create_app, db from myapp.models import User import pytest @pytest.fixture def app(): app = create_app('testing') with app.app_context(): db.create_all() yield app db.session.remove() db.drop_all() def test_index_ok(client): resp = client.get('/') assert resp.status_code == 200 def test_create_user_api(client): resp = client.post('/api/users', json={ 'name': 'alice', 'email': 'alice@example.com', }) assert resp.status_code == 201 data = resp.json assert data['name'] == 'alice' def test_duplicate_user_rejected(client): client.post('/api/users', json={ 'name': 'alice', 'email': 'alice@example.com', }) resp = client.post('/api/users', json={ 'name': 'alice2', 'email': 'alice@example.com', # 邮箱重复 }) assert resp.status_code == 400 # 被拒绝
运行:
pytest -v
输出中每个用例一行,PASS/FAIL 一目了然;FAIL 时给出断言细节与回溯——这就是回归的防线。
app、client、runner 三个 fixture 覆盖大多数场景;yield 处理清理。session_transaction() 直接注入登录态。pytest -v 让整套回归可重复执行。测试写得好不好,差别不在数量而在"测试什么、怎么组织"。几个策略性建议。
第一,测试金字塔的落地。 对 Flask 项目来说,最划算的是"视图/API 级测试"(用 test_client,覆盖路由、鉴权、校验、数据操作)与"纯函数测试"(工具函数、表单校验、模型方法)。端到端测试(真实浏览器)投入大、维护贵,只在核心流程用少量覆盖。把 80% 的测试预算花在 test_client 与纯函数上,性价比最高。
第二,测试的"独立性"是生命线。 每个测试都应该能单独跑、结果确定。依赖"上一个测试留下的数据"的测试,单个跑通过、全量跑挂——这种测试比没有更糟(因为它制造虚假的安全感)。保证独立性的手段:fixture 里建表删表、测试数据在用例内创建、不用共享的全局状态。
第三,断言要"验内容"而不是"验状态码"。 只断言 status_code == 200 的测试,页面渲染错了也测不出来。正确做法是同时断言关键内容片段:assert '欢迎,alice' in resp.get_data(as_text=True)。对 JSON API,直接断言 resp.json 里的字段值。状态码保证"没挂",内容保证"没错"——两者都要。
第四,鉴权测试的模式。 测"未登录被拦截"与"已登录能访问"两个方向:未登录直接访问 → 断言重定向或 401;已登录用 session_transaction() 注入登录态 → 断言 200。每个受保护路由都应该有这两个用例——鉴权逻辑的回归测试,是安全防线的一部分。
第五,测试数据库的隔离。 内存 SQLite(sqlite:///:memory:)最快但每次连接是独立库,需要注意连接配置;文件型测试库(如 test.db)可复用连接但要记得清理。绝对不要让测试连开发或生产数据库——一次误操作清空真实数据,教训是惨痛的。
第六,覆盖率不是目标,信心才是。 80% 覆盖率但测的都是无关紧要的代码,不如 40% 覆盖率但核心路径全测过。优先覆盖:业务规则、数据写入、鉴权、第三方调用边界。写测试时问自己:"如果这段代码被改坏,这个测试能发现吗?"回答不了,就换一个更有价值的测试。
第七,测试速度决定测试文化。 测试太慢,开发就不愿意跑。保持测试套件在几秒内跑完(test_client + 内存库),配合 pytest 的 -x(失败即停)与 -k(按名筛选)参数,让"写完代码跑一遍测试"成为无痛的习惯。