Flask Testing

Testing is the foundation of ensuring code quality.

Flask provides a built-in test client, which combined with pytest allows for efficient writing and running of tests.


Test Client——test_client

Flask'stest_client()The method creates a mocked HTTP client that can send requests without starting a server:

Example

# File path: test_demo.py (test in Python interactive environment)
from app import create_app

# Create a test app instance
app = create_app()

# Create a test client
with app.test_client() as client:
    # Send GET request
    response = client.get("/")
    print(response.status_code)   # 200
    print(response.data[:100])    # The first 100 bytes of the response body

    # Send POST request and send data as JSON
    response = client.post("/api/posts",
                          json={"title": "EXAMPLE Tutorial", "body": "content", "author_id": 1})
    print(response.status_code)   # 201
    print(response.get_json())    # Parse JSON response

pytest Integration

pytest is the standard testing framework in the Python community and works very conveniently with Flask.

Install pytest

(.venv) $ pip install pytest

Writing Tests——conftest.py (Shared Fixtures)

Usageconftest.pyThe file creates reusable test fixtures:

Example

# File path: 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():
    """Create a test app instance using a temporary database"""
    # Create a temporary file as test database
    db_fd, db_path = tempfile.mkstemp()

    # Create app, override database path to temporary file
    app = create_app()
    app.config["DATABASE"] = db_path
    app.config["TESTING"] = True  # Enable test mode

    # Initialize test database table structures
    with app.app_context():
        init_db()

    yield app

    # Clean up after tests: close and delete the temporary database file
    os.close(db_fd)
    os.unlink(db_path)

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

@pytest.fixture
def runner(app):
    """Create CLI test runner"""
    return app.test_cli_runner()

Test Routes

Example

# File path: tests/test_app.py
def test_home_page(client):
    """Test if the homepage returns normally"""
    response = client.get("/")
    assert response.status_code == 200
    # Check if the page contains expected keywords
    assert b"EXAMPLE" in response.data

def test_404_page(client):
    """Test visiting a non-existent page"""
    response = client.get("/this-page-does-not-exist")
    assert response.status_code == 404

Run tests:

(.venv) $ pytest tests/test_app.py -v
====================================
tests/test_app.py::test_home_page PASSED
tests/test_app.py::test_404_page PASSED
====================================

Test JSON API

Example

# File path: tests/test_api.py
def test_create_post(client):
    """Test create article API"""
    response = client.post("/api/posts", json={
        "title": "EXAMPLE test article",
        "body": "This is test content",
        "author_id": 1
    })
    # Successful creation should return 201
    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == "EXAMPLE test article"
    assert "id" in data

def test_create_post_without_title(client):
    """Test that empty title returns 400 error"""
    response = client.post("/api/posts", json={
        "title": "",
        "body": "content",
        "author_id": 1
    })
    assert response.status_code == 400
    assert b"error" in response.data

def test_get_posts(client):
    """Test getting article list"""
    # First create an article
    client.post("/api/posts", json={
        "title": "Article A", "body": "Content A", "author_id": 1
    })

    # Get list
    response = client.get("/api/posts")
    assert response.status_code == 200
    data = response.get_json()
    assert len(data) >= 1

def test_delete_post(client):
    """Test delete article"""
    # First create
    rv = client.post("/api/posts", json={
        "title": "To be deleted", "body": "content", "author_id": 1
    })
    post_id = rv.get_json()["id"]

    # Then delete
    rv = client.delete(f"/api/posts/{post_id}")
    assert rv.status_code == 200

    # Confirm deleted
    rv = client.get(f"/api/posts/{post_id}")
    assert rv.status_code == 404

Test Session

Testing features that require Session, such as login:

Example

# File path: tests/test_auth.py
def test_login(client):
    """Test login flow"""
    # Send login request
    response = client.post("/auth/login", data={
        "username": "testuser",
        "password": "testpass"
    }, follow_redirects=True)  # follow_redirects automatically follows redirects

    assert response.status_code == 200

def test_session_transaction(client):
    """Directly manipulate session for testing"""
    with client.session_transaction() as session:
        # Directly set values in session to simulate logged-in state
        session["username"] = "example"

    # Now visit the home page; it should show logged-in status
    response = client.get("/")
    assert b"example" in response.data

client.session_transaction()It is a very practical testing tool — it allows you to directly modify Session content without going through the complete login process.


Testing CLI commands

Example

# File path: tests/test_cli.py
def test_init_db(runner):
    """Test the database initialization command"""
    result = runner.invoke(args=["init-db"])
    # Command executed successfully; exit_code should be 0
    assert result.exit_code == 0

def test_flask_routes(runner):
    """Test the flask routes command"""
    result = runner.invoke(args=["routes"])
    assert result.exit_code == 0
    # Check whether the output contains registered routes
    assert "/api/posts" in result.output

Testing Best Practices

Practice Description
Using a Temporary Database Each test uses an independent database so they do not interfere with one another (e.g., tempfile in conftest.py)
Enabling TESTING mode app.config["TESTING"] = TrueLet exceptions propagate normally rather than being swallowed by error handling
One test should only test one thing. Each test function verifies only one behavior, making it easier to pinpoint issues
Clear naming Function names are liketest_<scenario>_<expected_result>
Make good use of follow_redirects Useful when testing the redirect flow after a POST

Run All Tests

(.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 ====================
other extensions