Unit testing: pytest writes high-coverage Flask tests

📂 Stage: Stage 5 - Advanced Advancement (Performance and Architecture) 🔗 Related chapters: Flask-Login 实战 · environment-setup

Every time I change the code, I dare not go online. Are I always worried that I have missed a route, form verification, or permissions are not configured properly? Stop using "handmade dots" to embolden yourself.

pytest + Flask is a golden combination to help you automatically verify business logic - it can not only simulate real requests, but also isolate the database. This tutorial will help you build a test suite from scratch that covers views, login authentication, and database operations, giving you more confidence when submitting code.


1. Zero threshold startup: installation and configuration of pytest

1.1 One line of commands to install necessary dependencies

Except for the corepytest, recommended to installpytest-flask, it can help you automatically handle the Flask application context and avoid manual tossing in tests.app_context()

pip install pytest pytest-flask

1.2 Add project root directorypytest.ini

With this configuration file, you don't need to add a bunch of parameters after the command line every time. build onepytest.ini, paste the following content in:

# pytest.ini
[pytest]
testpaths = tests          # 指定测试文件所在的目录
python_files = test_*.py   # 只扫描以 test_ 开头的文件
python_functions = test_*  # 只执行以 test_ 开头的函数
addopts = -v --tb=short    # 默认输出详细测试名,并精简错误堆栈

Then knock directlypytestwill automatically entertests/Directory, identificationtest_*.pyfiles, saving a lot of repetitive work.


2. No more repetitive code: pytest Fixtures in practice

If every time you write a test function, you have to manually create an application, create a table, register a user, and log in, the test code will soon become a hell of "eight-part essay".

conftest.pyIt is the global scaffolding file provided by pytest. We put the reusable pre/post logic here, usingfixtureEncapsulate it. Each test function can automatically obtain a clean environment by declaring which fixture is required.

# conftest.py(放在项目根目录或 tests/ 目录下)
import pytest
from app import create_app
from app.extensions import db

@pytest.fixture
def app():
    """创建、初始化并在测试结束后销毁 Flask 应用"""
    # 用 testing 配置创建应用(确保与开发/生产隔离)
    app = create_app("testing")

    # 在应用上下文中建表 → 测试 → 最后删表
    with app.app_context():
        db.create_all()
        yield app            # 把应用交给测试函数
        db.drop_all()        # 测试完成后自动清理数据

@pytest.fixture
def client(app):
    """模拟浏览器的测试客户端,用来发 GET/POST 请求"""
    return app.test_client()

@pytest.fixture
def authenticated_client(client, app):
    """直接提供一个已登录的客户端,省得每个用例都写一遍登录流程"""
    with app.app_context():
        client.post("/auth/register", data={
            "email": "authed_test@example.com",
            "password": "TestPass123!",
            "username": "authed_testuser"
        })
    client.post("/auth/login", data={
        "email": "authed_test@example.com",
        "password": "TestPass123!"
    })
    return client

Tips:yieldThe previous code is run before the test function is executed, and the subsequent code is run after the test is completed. This is the common paradigm to implement "create tables before testing and clear them after testing".


3. Basic view test: from home page to login and registration

Let's start with the simplest, stateless public pages - homepage, login page, registration page. This type of testing is less error-prone and can help you quickly build confidence in the testing framework.

# tests/test_auth.py
def test_index_page_loads(client):
    """✅ 验证首页能正常打开,并包含核心文案"""
    response = client.get("/")
    # 1. HTTP 状态码必须是 200
    assert response.status_code == 200
    # 2. 页面内容里应当有标志性的文字(response.data 是 bytes,用 b"" 匹配)
    assert b"道满博客" in response.data

def test_login_page_renders_form(client):
    """✅ 验证登录页能正常渲染并展示表单"""
    response = client.get("/auth/login")
    assert response.status_code == 200
    # 检查是否包含邮箱输入框(可以是 input 标签或文本提示)
    assert b'<input type="email"' in response.data or b"邮箱" in response.data

def test_login_success_with_valid_credentials(client, app):
    """✅ 使用正确的邮箱和密码登录后,应重定向并显示成功提示"""
    # 先注册一个测试用户
    client.post("/auth/register", data={
        "email": "alice@test.com",
        "password": "StrongPass!2024",
        "username": "alice_dev"
    })

    # 登录并自动跟随重定向
    response = client.post("/auth/login", data={
        "email": "alice@test.com",
        "password": "StrongPass!2024"
    }, follow_redirects=True)

    assert response.status_code == 200
    assert b"登录成功" in response.data

def test_login_rejects_wrong_password(client):
    """❌ 错误密码应被拦截,并返回相应的错误信息"""
    response = client.post("/auth/login", data={
        "email": "nonexistent@test.com",
        "password": "wrong123"
    })
    assert response.status_code == 200
    assert b"邮箱或密码错误" in response.data

def test_register_requires_valid_fields(client):
    """❌ 注册时使用无效的邮箱、过短的密码,应被表单校验挡住"""
    response = client.post("/auth/register", data={
        "email": "not-a-real-email",
        "password": "123",
        "username": "t"
    })
    assert response.status_code == 200
    # 只要看到一个校验错误,就说明表单验证在工作了
    assert b"邮箱格式不正确" in response.data or b"password" in response.data

Regardless of whether it is a "success path" or a "failure path", each test only focuses on one scenario. In this way, when the use case fails, you can immediately know which specific logic has the problem.


4. Advanced test: verify login protection and permissions

Using Flask-Login@login_requiredFinally we have to confirm:

  • Not logged in users will be redirected to the login page.
  • Logged in users can access and perform operations normally.
# tests/test_articles.py
def test_create_article_redirects_to_login_when_not_authed(client):
    """⚠️ 未登录时访问创建文章页面,必须被重定向到登录页"""
    response = client.get("/articles/create")
    # 302 表示临时重定向
    assert response.status_code == 302
    # 重定向地址应当指向登录相关路径
    assert response.location.startswith("/auth/login")

def test_authed_user_can_create_article(authenticated_client):
    """✅ 已登录的用户能成功创建文章,并看到文章标题"""
    response = authenticated_client.post("/articles/create", data={
        "title": "我的第一篇测试博客",
        "content": "这是专门用来测试博客创建功能的内容~"
    }, follow_redirects=True)

    assert response.status_code == 200
    assert b"我的第一篇测试博客" in response.data

Here'sauthenticated_clientfrom us atconftest.pyThe fixtures defined in , completely eliminate the trouble of repeated login.


5. Give your code a "physical examination": test coverage

After writing a bunch of tests, how much business code is covered? usepytest-covIntuitive visual reports can be generated to see at a glance which lines of code have never been executed.

5.1 Installation and operation

pip install pytest-cov

# 终端里输出哪一行没有被覆盖(term-missing)
pytest --cov=app --cov-report=term-missing

# 生成 HTML 报告(推荐),可视化查看每一行代码的覆盖情况
pytest --cov=app --cov-report=html
# 运行后打开 htmlcov/index.html 即可查看

5.2 Small coverage goals

Don’t be held hostage by “100% coverage.” It is recommended that core processes (login, registration, protected operations) reach 80% or more, and tool functions and simple logic can pursue 100%. Prioritize ensuring that those scenarios with “more bad news than good news” are covered by tests.


6. Checklist & Best Practices

📝 Commonly used pytest + Flask test syntax quick check

@pytest.fixture          # 定义可复用的测试前置/后置
client.get(url)          # 发送 GET 请求
client.post(url, data={...})  # 发送 POST 请求(form-data 格式)
response.status_code     # 获取 HTTP 状态码
response.data            # 获取响应体(bytes 类型,用 b"" 匹配字符串)
response.location        # 获取重定向的目标 URL
follow_redirects=True    # 自动跟随重定向,获取最终响应

💡 Best Practice Tips

  1. Test naming should be done in human language: Just look at the function name to know whether it is testing "success", "failure" or "boundary case".
  2. A use case only tests one thing: Don’t put registration, login, and article posting into the sametest_*inside.
  3. Only use the database in a test environment: Never touch the development or production database, be sure to use independent configurations and instances.
  4. Try TDD (Test Driven Development): First write a failing test, then write code to make it green, and finally refactor.

🔗 Extended reading