backend intermediate

Testing Flask Routes with pytest

3 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.