Flask 教程

Flask 测试

编写测试是构建健壮 Flask 应用的关键环节。良好的测试能确保你在重构代码或添加新功能时,不会意外破坏现有逻辑。

Flask 内置了强大的测试客户端(基于 Werkzeug),并结合 Python 标准的 unittest 框架或更现代的 pytest,可以非常方便地进行单元测试和集成测试。


🛠️ 1. 选择测试框架

虽然 Python 自带 unittest,但社区更推荐使用 pytest。它语法更简洁,插件生态更丰富(如 pytest-cov 用于覆盖率检查)。

安装依赖

纯文本
plaintext
pip install pytest pytest-cov

🏗️ 2. 测试环境配置

在运行测试时,我们通常希望:

  1. 使用一个独立的测试数据库(避免污染开发/生产数据)。
  2. 开启 TESTING 模式(这会禁用某些错误捕获机制,让调试更容易)。
  3. 关闭 CSRF 保护(简化表单提交测试)。

config.py 中添加测试配置:

纯文本
plaintext
import os

class TestingConfig(Config):
    TESTING = True
    WTF_CSRF_ENABLED = False  # 关闭 CSRF,方便测试 POST 请求
    # 使用内存 SQLite 数据库,速度极快且每次测试后自动清空
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' 
    SQLALCHEMY_TRACK_MODIFICATIONS = False

🧪 3. 设置测试夹具 (Fixtures)

使用 pytestconftest.py 文件来定义全局可用的“夹具”(Fixtures)。这是测试的核心,负责创建应用实例、数据库会话和测试客户端。

在项目根目录或 tests/ 目录下创建 conftest.py

纯文本
plaintext
import pytest
from app import create_app
from app.extensions import db
from config import TestingConfig

@pytest.fixture
def app():
    """创建并配置一个用于测试的应用实例"""
    app = create_app(TestingConfig)

    with app.app_context():
        # 创建所有表
        db.create_all()

        # 在这里可以插入一些初始测试数据
        # from app.models import User
        # user = User(username='testuser', email='test@example.com')
        # db.session.add(user)
        # db.session.commit()

        yield app

        # 测试结束后清理数据库
        db.drop_all()

@pytest.fixture
def client(app):
    """创建一个测试客户端"""
    return app.test_client()

@pytest.fixture
def runner(app):
    """创建一个 CLI 运行器,用于测试命令行命令"""
    return app.test_cli_runner()

📝 4. 编写测试用例

A. 测试视图函数 (Integration Test)

测试 HTTP 请求和响应是否正确。

创建 tests/test_views.py:

纯文本
plaintext
import json

def test_index_page(client):
    """测试首页是否正常返回 200"""
    response = client.get('/')
    assert response.status_code == 200
    assert b'Welcome' in response.data  # 检查返回内容中是否包含特定字节

def test_404_page(client):
    """测试访问不存在页面返回 404"""
    response = client.get('/nonexistent')
    assert response.status_code == 404

def test_api_json_response(client):
    """测试 API 返回 JSON 数据"""
    response = client.get('/api/data')
    assert response.status_code == 200
    assert response.content_type == 'application/json'

    data = json.loads(response.data)
    assert 'status' in data
    assert data['status'] == 'success'

B. 测试表单提交 (POST Request)

纯文本
plaintext
def test_login_success(client):
    """测试登录成功"""
    # 模拟 POST 请求提交表单
    response = client.post('/login', data={
        'username': 'admin',
        'password': 'secret'
    }, follow_redirects=True)  # 跟随重定向

    assert response.status_code == 200
    assert b'Welcome back' in response.data

def test_login_failure(client):
    """测试登录失败"""
    response = client.post('/login', data={
        'username': 'admin',
        'password': 'wrong_password'
    })

    assert response.status_code == 200 # 通常登录失败也返回 200,但显示错误信息
    assert b'Invalid credentials' in response.data

C. 测试数据库模型 (Unit Test)

直接测试模型逻辑,不经过 HTTP 层。

创建 tests/test_models.py:

纯文本
plaintext
from app.models import User
from app.extensions import db

def test_create_user(app):
    """测试创建用户"""
    with app.app_context():
        user = User(username='john', email='john@example.com')
        db.session.add(user)
        db.session.commit()

        # 验证用户已保存
        saved_user = User.query.filter_by(username='john').first()
        assert saved_user is not None
        assert saved_user.email == 'john@example.com'

def test_unique_username(app):
    """测试用户名唯一性约束"""
    with app.app_context():
        user1 = User(username='duplicate', email='a@example.com')
        user2 = User(username='duplicate', email='b@example.com')

        db.session.add(user1)
        db.session.commit()

        db.session.add(user2)

        # 预期会抛出 IntegrityError
        try:
            db.session.commit()
            assert False, "Expected IntegrityError"
        except Exception as e:
            db.session.rollback() # 重要:回滚事务
            assert 'UNIQUE constraint failed' in str(e)

🚀 5. 运行测试

在项目根目录下执行:

纯文本
plaintext
# 运行所有测试
pytest

# 运行指定文件的测试
pytest tests/test_views.py

# 运行并显示详细输出
pytest -v

# 生成覆盖率报告 (需要安装 pytest-cov)
pytest --cov=app --cov-report=html

执行 pytest --cov-report=html 后,会在 htmlcov/ 目录下生成可视化的覆盖率报告,告诉你哪些代码行还没被测试覆盖。


💡 6. 高级技巧与最佳实践

A. 模拟外部服务 (Mocking)

如果你的视图函数调用了第三方 API(如发送短信、支付接口),不要在测试中真正调用它们。使用 unittest.mock 进行模拟。

纯文本
plaintext
from unittest.mock import patch

@patch('app.services.send_sms')  # 模拟 send_sms 函数
def test_register_sends_sms(mock_send_sms, client):
    mock_send_sms.return_value = True

    response = client.post('/register', data={'phone': '123456'})

    # 验证 send_sms 是否被调用了一次
    mock_send_sms.assert_called_once_with('123456')
    assert response.status_code == 201

B. 测试认证保护的路由

如果路由使用了 @login_required,你需要先登录获取 Session。

纯文本
plaintext
def test_protected_route(client):
    # 1. 先登录
    client.post('/login', data={'username': 'admin', 'password': 'secret'})

    # 2. 访问受保护路由
    response = client.get('/dashboard')
    assert response.status_code == 200

C. 保持测试独立

每个测试函数应该是独立的。利用 app fixture 中的 db.create_all()db.drop_all() 确保每个测试都在干净的数据库中运行。不要依赖测试执行的顺序。


📝 总结

组件作用
pytest测试运行器,语法简洁
conftest.py定义全局 Fixtures (app, client, db)
test_client模拟浏览器发送 HTTP 请求
Mock模拟外部依赖 (API, Email, SMS)
Coverage检查代码测试覆盖率

最佳实践:

  1. 测试驱动开发 (TDD):先写测试,再写代码。
  2. 命名规范:测试文件以 test_ 开头,测试函数以 test_ 开头。
  3. 隔离性:测试之间互不影响,使用内存数据库或事务回滚。
  4. 持续集成 (CI):将 pytest 命令加入 GitHub Actions 或 GitLab CI,每次提交代码自动运行测试。

掌握了测试,你的 Flask 应用就拥有了“安全网”,可以放心大胆地迭代和优化。恭喜你,至此你已经完成了 Flask 从入门到精通的全套学习旅程!🎉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注