Testing Flask Routes with pytest
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
pyteststep 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.