2.6 测试 Testing


2.6 测试 Testing

问题与直觉:为什么必须自动化测试

应用上线的代码一旦改错,最典型的问题:改 A 功能,坏了 B 功能。手动测试的痛点:费时、容易漏、没人愿意每次改完都全量点一遍。

自动化测试 = 把"验证行为"变成可重复执行的代码。Flask 的杀手锏是 test_client不发真实网络请求,直接在内存里模拟一次完整请求,跑完立刻断言响应。

💡 关键直觉:test_client 就像"替身考官"——真实浏览器要起服务器、发请求、收响应;test_client 让 Flask 直接"内部考一遍",快几个数量级。

核心原理:test_client 与测试环境

2.1 第一个测试

# 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:响应头

2.2 测试 POST 与表单

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

工程实践要点:pytest + Flask

3.1 安装与目录结构

pip install pytest

推荐结构:

project/ ├── app/ # 应用包(用工厂函数 create_app) ├── tests/ │ ├── conftest.py # 共享 fixture │ ├── test_views.py │ └── test_api.py └── requirements.txt

3.2 conftest.py:共享 fixture

# 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

3.3 测试数据库隔离

原则:测试绝不能碰开发/生产数据库。常见做法:

  • 测试配置用独立数据库文件(如 sqlite:////tmp/test.db 或内存库);
  • 每个测试(或每个测试类)重建表,保证互不污染。
# config 中的测试配置 class TestingConfig(Config): TESTING = True DATABASE_URL = 'sqlite:///:memory:' # 内存数据库,跑完即弃 WTF_CSRF_ENABLED = False # 测试时关掉 CSRF 简化表单测试

3.4 测试登录会话

需要"已登录"状态的接口,可以在测试中直接设置 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,不必真走一遍登录流程。

3.5 测试蓝图与错误页

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)

测试金字塔与取舍

测试金字塔:投入与收益

测试金字塔:投入与收益

取舍原则

  • 不是每行代码都要测:优先测"业务逻辑、数据操作、鉴权、API 契约";
  • 测试要快:内存数据库 + test_client,整个测试套件几秒跑完;
  • 先写会挂的测试:新功能先写断言再实现,即 TDD 的基本节奏;
  • 维护成本要低:fixture 集中、命名清晰、断言具体(别只断言 200)。

常见误区与排查

误区 现象 正解
测试连上开发数据库 测试污染真实数据 用 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 时给出断言细节与回溯——这就是回归的防线

本节速览

  • test_client:不发真实请求,内存中模拟 GET/POST/PUT/DELETE,快且稳。
  • pytest + fixtureappclientrunner 三个 fixture 覆盖大多数场景;yield 处理清理。
  • 数据库隔离:TestingConfig 用内存库,测试前建表、测试后删表。
  • 会话测试session_transaction() 直接注入登录态。
  • 金字塔取舍:视图/API/数据库操作是重点,纯函数必测,端到端少而精。
  • 一条命令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(按名筛选)参数,让"写完代码跑一遍测试"成为无痛的习惯。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U