Flask 测试
测试是保证代码质量的基础。
Flask 提供了内置的测试客户端,配合 pytest 可以高效编写和运行测试。
测试客户端——test_client
Flask 的 test_client() 方法创建了一个模拟的 HTTP 客户端,可以在不启动服务器的情况下发送请求:
实例
# 文件路径:test_demo.py(在 Python 交互环境中测试)
from app import create_app
# 创建测试用的 app 实例
app = create_app()
# 创建测试客户端
with app.test_client() as client:
# 发送 GET 请求
response = client.get("/")
print(response.status_code) # 200
print(response.data[:100]) # 响应体的前 100 个字节
# 发送 POST 请求并以 JSON 发送数据
response = client.post("/api/posts",
json={"title": "RUNOOB 教程", "body": "内容", "author_id": 1})
print(response.status_code) # 201
print(response.get_json()) # 解析 JSON 响应
from app import create_app
# 创建测试用的 app 实例
app = create_app()
# 创建测试客户端
with app.test_client() as client:
# 发送 GET 请求
response = client.get("/")
print(response.status_code) # 200
print(response.data[:100]) # 响应体的前 100 个字节
# 发送 POST 请求并以 JSON 发送数据
response = client.post("/api/posts",
json={"title": "RUNOOB 教程", "body": "内容", "author_id": 1})
print(response.status_code) # 201
print(response.get_json()) # 解析 JSON 响应
pytest 集成
pytest 是 Python 社区的标准测试框架,配合 Flask 使用非常方便。
安装 pytest
(.venv) $ pip install pytest
编写测试——conftest.py(共享夹具)
使用 conftest.py 文件创建可复用的测试夹具(fixture):
实例
# 文件路径:tests/conftest.py
import os
import tempfile
import pytest
from app import create_app
from db import init_db, get_db
@pytest.fixture
def app():
"""创建测试用的 app 实例,使用临时数据库"""
# 创建临时文件作为测试数据库
db_fd, db_path = tempfile.mkstemp()
# 创建 app,覆盖数据库路径到临时文件
app = create_app()
app.config["DATABASE"] = db_path
app.config["TESTING"] = True # 启用测试模式
# 初始化测试数据库表结构
with app.app_context():
init_db()
yield app
# 测试后清理:关闭并删除临时数据库文件
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""创建测试客户端"""
return app.test_client()
@pytest.fixture
def runner(app):
"""创建 CLI 测试运行器"""
return app.test_cli_runner()
import os
import tempfile
import pytest
from app import create_app
from db import init_db, get_db
@pytest.fixture
def app():
"""创建测试用的 app 实例,使用临时数据库"""
# 创建临时文件作为测试数据库
db_fd, db_path = tempfile.mkstemp()
# 创建 app,覆盖数据库路径到临时文件
app = create_app()
app.config["DATABASE"] = db_path
app.config["TESTING"] = True # 启用测试模式
# 初始化测试数据库表结构
with app.app_context():
init_db()
yield app
# 测试后清理:关闭并删除临时数据库文件
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""创建测试客户端"""
return app.test_client()
@pytest.fixture
def runner(app):
"""创建 CLI 测试运行器"""
return app.test_cli_runner()
测试路由
实例
# 文件路径:tests/test_app.py
def test_home_page(client):
"""测试首页是否正常返回"""
response = client.get("/")
assert response.status_code == 200
# 检查页面是否包含预期关键字
assert b"RUNOOB" in response.data
def test_404_page(client):
"""测试访问不存在的页面"""
response = client.get("/this-page-does-not-exist")
assert response.status_code == 404
def test_home_page(client):
"""测试首页是否正常返回"""
response = client.get("/")
assert response.status_code == 200
# 检查页面是否包含预期关键字
assert b"RUNOOB" in response.data
def test_404_page(client):
"""测试访问不存在的页面"""
response = client.get("/this-page-does-not-exist")
assert response.status_code == 404
运行测试:
(.venv) $ pytest tests/test_app.py -v ==================================== tests/test_app.py::test_home_page PASSED tests/test_app.py::test_404_page PASSED ====================================
测试 JSON API
实例
# 文件路径:tests/test_api.py
def test_create_post(client):
"""测试创建文章 API"""
response = client.post("/api/posts", json={
"title": "RUNOOB 测试文章",
"body": "这是测试内容",
"author_id": 1
})
# 创建成功应返回 201
assert response.status_code == 201
data = response.get_json()
assert data["title"] == "RUNOOB 测试文章"
assert "id" in data
def test_create_post_without_title(client):
"""测试标题为空时返回 400 错误"""
response = client.post("/api/posts", json={
"title": "",
"body": "内容",
"author_id": 1
})
assert response.status_code == 400
assert b"error" in response.data
def test_get_posts(client):
"""测试获取文章列表"""
# 先创建一篇文章
client.post("/api/posts", json={
"title": "文章 A", "body": "内容 A", "author_id": 1
})
# 获取列表
response = client.get("/api/posts")
assert response.status_code == 200
data = response.get_json()
assert len(data) >= 1
def test_delete_post(client):
"""测试删除文章"""
# 先创建
rv = client.post("/api/posts", json={
"title": "待删除", "body": "内容", "author_id": 1
})
post_id = rv.get_json()["id"]
# 再删除
rv = client.delete(f"/api/posts/{post_id}")
assert rv.status_code == 200
# 确认已删除
rv = client.get(f"/api/posts/{post_id}")
assert rv.status_code == 404
def test_create_post(client):
"""测试创建文章 API"""
response = client.post("/api/posts", json={
"title": "RUNOOB 测试文章",
"body": "这是测试内容",
"author_id": 1
})
# 创建成功应返回 201
assert response.status_code == 201
data = response.get_json()
assert data["title"] == "RUNOOB 测试文章"
assert "id" in data
def test_create_post_without_title(client):
"""测试标题为空时返回 400 错误"""
response = client.post("/api/posts", json={
"title": "",
"body": "内容",
"author_id": 1
})
assert response.status_code == 400
assert b"error" in response.data
def test_get_posts(client):
"""测试获取文章列表"""
# 先创建一篇文章
client.post("/api/posts", json={
"title": "文章 A", "body": "内容 A", "author_id": 1
})
# 获取列表
response = client.get("/api/posts")
assert response.status_code == 200
data = response.get_json()
assert len(data) >= 1
def test_delete_post(client):
"""测试删除文章"""
# 先创建
rv = client.post("/api/posts", json={
"title": "待删除", "body": "内容", "author_id": 1
})
post_id = rv.get_json()["id"]
# 再删除
rv = client.delete(f"/api/posts/{post_id}")
assert rv.status_code == 200
# 确认已删除
rv = client.get(f"/api/posts/{post_id}")
assert rv.status_code == 404
测试 Session
测试登录等需要 Session 的功能:
实例
# 文件路径:tests/test_auth.py
def test_login(client):
"""测试登录流程"""
# 发送登录请求
response = client.post("/auth/login", data={
"username": "testuser",
"password": "testpass"
}, follow_redirects=True) # follow_redirects 自动跟随重定向
assert response.status_code == 200
def test_session_transaction(client):
"""直接操作 session 进行测试"""
with client.session_transaction() as session:
# 直接在 session 中设置值,模拟已登录状态
session["username"] = "runoob"
# 现在访问首页,应该显示已登录状态
response = client.get("/")
assert b"runoob" in response.data
def test_login(client):
"""测试登录流程"""
# 发送登录请求
response = client.post("/auth/login", data={
"username": "testuser",
"password": "testpass"
}, follow_redirects=True) # follow_redirects 自动跟随重定向
assert response.status_code == 200
def test_session_transaction(client):
"""直接操作 session 进行测试"""
with client.session_transaction() as session:
# 直接在 session 中设置值,模拟已登录状态
session["username"] = "runoob"
# 现在访问首页,应该显示已登录状态
response = client.get("/")
assert b"runoob" in response.data
client.session_transaction() 是一个非常实用的测试工具——它让你可以直接修改 Session 内容,无需通过完整的登录流程。
测试 CLI 命令
实例
# 文件路径:tests/test_cli.py
def test_init_db(runner):
"""测试数据库初始化命令"""
result = runner.invoke(args=["init-db"])
# 命令执行成功,exit_code 应为 0
assert result.exit_code == 0
def test_flask_routes(runner):
"""测试 flask routes 命令"""
result = runner.invoke(args=["routes"])
assert result.exit_code == 0
# 检查输出是否包含注册的路由
assert "/api/posts" in result.output
def test_init_db(runner):
"""测试数据库初始化命令"""
result = runner.invoke(args=["init-db"])
# 命令执行成功,exit_code 应为 0
assert result.exit_code == 0
def test_flask_routes(runner):
"""测试 flask routes 命令"""
result = runner.invoke(args=["routes"])
assert result.exit_code == 0
# 检查输出是否包含注册的路由
assert "/api/posts" in result.output
测试最佳实践
| 实践 | 说明 |
|---|---|
| 使用临时数据库 | 每次测试用独立数据库,互不干扰(如 conftest.py 中的 tempfile) |
| 开启 TESTING 模式 | app.config["TESTING"] = True 让异常正常传播而非被错误处理吞掉 |
| 一个测试只测一件事 | 每个 test 函数只验证一个行为,便于定位问题 |
| 命名清晰 | 函数名形如 test_什么场景_预期什么结果 |
| 善用 follow_redirects | 测试 POST 后重定向的流程时很有用 |
运行全部测试
(.venv) $ pytest tests/ -v tests/test_app.py::test_home_page PASSED tests/test_app.py::test_404_page PASSED tests/test_api.py::test_create_post PASSED tests/test_api.py::test_create_post_without_title PASSED tests/test_api.py::test_get_posts PASSED tests/test_api.py::test_delete_post PASSED tests/test_auth.py::test_login PASSED tests/test_auth.py::test_session_transaction PASSED tests/test_cli.py::test_init_db PASSED tests/test_cli.py::test_flask_routes PASSED ==================== 10 passed in 0.45s ====================
