backend intermediate

Testing Flask Routes with pytest

min read Frederick Tubiermont

Testing Flask Routes with pytest

Many Flask developers skip testing because it feels complex. It is not. Flask ships with a test client that lets you simulate HTTP requests without a running server, and pytest makes fixtures and assertions clean. This tutorial gets you to a working test suite in under an hour.

Install pytest

pip install pytest

That is all. No extra Flask extension needed.

Project Structure

your-app/
  app.py
  utils/
    db.py
    email.py
  tests/
    conftest.py      ← fixtures shared across all tests
    test_routes.py   ← route tests
    test_api.py      ← API tests

conftest.py: The Test Client Fixture

# tests/conftest.py
import pytest
import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from app import app as flask_app

@pytest.fixture
def app():
    flask_app.config.update({
        "TESTING": True,
        "SECRET_KEY": "test-secret-key",
        "WTF_CSRF_ENABLED": False
    })
    yield flask_app

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

The client fixture gives every test a fresh test client. TESTING = True makes Flask propagate exceptions instead of returning 500 pages, which makes failures easier to debug.

Testing a GET Route

# tests/test_routes.py

def test_home_returns_200(client):
    response = client.get("/")
    assert response.status_code == 200

def test_home_contains_expected_text(client):
    response = client.get("/")
    assert b"Flask Vibe" in response.data

def test_nonexistent_route_returns_404(client):
    response = client.get("/this-does-not-exist")
    assert response.status_code == 404

Testing a POST Route

def test_subscribe_valid_email(client):
    response = client.post("/subscribe", data={
        "email": "[email protected]"
    }, follow_redirects=True)
    assert response.status_code == 200

def test_subscribe_invalid_email(client):
    response = client.post("/subscribe", data={
        "email": "not-an-email"
    })
    # Expect a 400 or redirect back with error
    assert response.status_code in (200, 302, 400)

follow_redirects=True follows any redirect chain and returns the final response.

Testing JSON API Routes

import json

def test_api_returns_json(client):
    response = client.get("/api/tutorials")
    assert response.status_code == 200
    assert response.content_type == "application/json"

def test_api_tutorials_structure(client):
    response = client.get("/api/tutorials")
    data = json.loads(response.data)
    assert "data" in data
    assert isinstance(data["data"], list)

def test_api_tutorial_detail(client):
    response = client.get("/api/tutorials/getting-started")
    data = json.loads(response.data)
    assert data["data"]["slug"] == "getting-started"

def test_api_tutorial_not_found(client):
    response = client.get("/api/tutorials/does-not-exist")
    assert response.status_code == 404

Mocking External Calls

When a route calls the OpenAI API or sends email, you do not want real calls during tests. Use unittest.mock:

from unittest.mock import patch

def test_summarize_does_not_call_openai_on_empty_input(client):
    response = client.post("/api/summarize", json={"text": ""})
    assert response.status_code == 400

def test_summarize_calls_openai(client):
    with patch("utils.ai.openai.chat.completions.create") as mock_openai:
        mock_openai.return_value.choices = [
            type("obj", (object,), {
                "message": type("msg", (object,), {"content": "Mocked summary"})()
            })()
        ]
        response = client.post("/api/summarize", json={"text": "Long article text here"})
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["summary"] == "Mocked summary"
        assert mock_openai.called

def test_register_sends_welcome_email(client):
    with patch("utils.email.send_email") as mock_send:
        mock_send.return_value = True
        client.post("/register", data={
            "name": "Alice",
            "email": "[email protected]",
            "password": "securepassword123"
        })
        assert mock_send.called
        call_args = mock_send.call_args[1]
        assert call_args["to"] == "[email protected]"

Running Tests

# Run all tests
pytest

# Run with output
pytest -v

# Run a specific file
pytest tests/test_routes.py

# Stop at first failure
pytest -x

# Run tests matching a name pattern
pytest -k "test_api"

Marking Slow Tests

Tag tests that hit external services so you can skip them locally:

import pytest

@pytest.mark.slow
def test_real_openai_call(client):
    # Only run with: pytest -m slow
    ...

Register the mark in pytest.ini:

[pytest]
markers =
    slow: marks tests as slow (deselect with -m "not slow")

AI Prompt That Generated This

"Write pytest tests for a Flask app. Include a conftest.py with app and client fixtures. Test a GET route, a POST route with form data, a JSON API route, and show how to mock an external API call using unittest.mock.patch. No special Flask-testing libraries — just pytest and Flask test client."

Next Steps

  • Add tests for your authentication routes using session fixtures
  • Run tests in CI by adding a pytest step to your Railway or GitHub Actions config

Was this helpful?

Get More Flask Vibe Tutorials

Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.

No spam. Unsubscribe anytime.