下载工作台
Flask Web 开发

测试、安全与生产部署

试读上半部分 · 解锁后可读全文

第 11 章 · 测试、安全与生产部署

本章目标:使用 pytest 与 Flask test client fixture 编写 API 与视图测试;理解 SECRET_KEY 与敏感配置管理;掌握 DEBUG=False 行为差异;配置 Gunicorn + Nginx 反向代理与静态/上传托管;设置 HTTPS 相关代理头;为 api-demo 建立可上线的最小安全清单。

学时建议:5~6 小时(含 2 小时部署演练)

前置:本模块 ch01~ch10python-dev ch10 虚拟环境与依赖。运维概念可对照 ops-deploy 模块。


11.1 测试金字塔(Flask 视角)

        ┌─────────┐
        │ E2E 少量 │  Playwright(选修)
        ├─────────┤
        │ API 集成 │  pytest + Flask test client
        ├─────────┤
        │ 单元较多 │  utils、序列化、表单校验
        └─────────┘
层级工具api-demo 示例
单元pytestsave_cover 扩展名校验
集成test client商品 CRUD API
E2E浏览器管理页登录(毕业项目加分)
本章测试均针对虚构 api-demo,不依赖任何企业内部 CI 或私有镜像仓库。

11.2 pytest 环境搭建

pip install pytest pytest-cov

pytest.ini(项目根):

[pytest]
testpaths = tests
python_files = test_*.py
addopts = -ra --strict-markers

tests/conftest.py

import pytest
from api_demo import create_app
from api_demo.extensions import db


@pytest.fixture
def app():
    application = create_app("testing")
    with application.app_context():
        db.create_all()
        yield application
        db.session.remove()
        db.drop_all()


@pytest.fixture
def client(app):
    return app.test_client()


@pytest.fixture
def runner(app):
    return app.test_cli_runner()


@pytest.fixture
def auth_client(client, app):
    """创建用户并登录,返回带 session 的 client。"""
    from api_demo.models.user import User
    with app.app_context():
        user = User(username="tester", email="test@user-demo.example.com")
        user.set_password("TestPass123!")
        db.session.add(user)
        db.session.commit()
    client.post("/auth/login", data={
        "username": "tester",
        "password": "TestPass123!",
    })
    return client

运行:

pytest
pytest tests/test_api_products.py -v
pytest --cov=api_demo --cov-report=term-missing

11.3 API 测试示例

tests/test_api_products.py

import json
from decimal import Decimal
from api_demo.models.product import Product
from api_demo.extensions import db


def test_list_products_empty(client):
    res = client.get("/api/v1/products")
    assert res.status_code == 200
    body = res.get_json()
    assert body["code"] == 0
    assert body["data"] == []
    assert body["pagination"]["total"] == 0


def test_list_products_with_data(client, app):
    with app.app_context():
        p = Product(name="测试商品", slug="test-item", price=Decimal("29.9"), is_published=True)
        db.session.add(p)
        db.session.commit()

    res = client.get("/api/v1/products")
    body = res.get_json()
    assert body["pagination"]["total"] == 1
    assert body["data"][0]["name"] == "测试商品"


def test_create_product_requires_auth(client):
    res = client.post(
        "/api/v1/products",
        data=json.dumps({"name": "新品", "slug": "new", "price": "10"}),
        content_type="application/json",
    )
    assert res.status_code == 401
    assert res.get_json()["code"] == 40101


def test_create_product_authenticated(auth_client, app):
    res = auth_client.post(
        "/api/v1/products",
        data=json.dumps({"name": "新品", "slug": "new-sku", "price": "99.00", "stock": 10}),
        content_type="application/json",
    )
    assert res.status_code == 201
    assert res.get_json()["data"]["slug"] == "new-sku"

    with app.app_context():
        assert Product.query.filter_by(slug="new-sku").count() == 1


def test_not_found_json(client):
    res = client.get("/api/v1/products/99999")
    assert res.status_code == 404
    assert res.get_json()["code"] == 40400
断言说明
status_codeHTTP 层
body["code"]业务层(ch07 约定)
app.app_context()测试中访问 db 需上下文

11.4 表单与上传测试

def test_upload_cover(auth_client, app):
    with app.app_context():
        p = Product(name="封面试", slug="cover-test", price=1)
        db.session.add(p)
        db.session.commit()
        pid = p.id

    from io import BytesIO
    data = {"file": (BytesIO(b"fake-image-bytes"), "test.jpg")}
    res = auth_client.post(f"/api/v1/products/{pid}/cover", data=data, content_type="multipart/form-data")
    assert res.status_code == 200
    assert "cover_url" in res.get_json()["data"]

TestingConfigUPLOAD_FOLDER 指向临时目录,测试结束可清理。


11.5 DEBUG=False 的影响

class ProductionConfig(Config):
    DEBUG = False
    TESTING = False
DEBUG=TrueDEBUG=False
Werkzeug 交互式调试器(切勿用于生产)通用 500 页
详细错误堆栈返回浏览器堆栈仅写日志
send_from_directory 托管上传(ch08)必须 Nginx 提供 /uploads/

本地验证生产配置

APP_ENV=production SECRET_KEY=prod-test-key flask run
# 故意访问 /api/v1/boom 应无堆栈泄露

11.6 SECRET_KEY 与安全配置

禁止将真实 SECRET_KEY、数据库密码提交 Git。

.env.example

APP_ENV=production
SECRET_KEY=change-me-use-openssl-rand-hex-32

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。