Overview
Flask provides excellent support for testing applications through theFlaskClient and FlaskCliRunner test utilities. These tools make it easy to test your views, CLI commands, and application behavior.
Test Client Basics
Setting Up Tests
import pytest
from myapp import create_app, db
@pytest.fixture
def app():
"""Create application for testing."""
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def runner(app):
"""Create CLI runner."""
return app.test_cli_runner()
Basic Request Testing
def test_index(client):
"""Test index page."""
response = client.get('/')
assert response.status_code == 200
assert b'Welcome' in response.data
def test_404(client):
"""Test 404 error."""
response = client.get('/nonexistent')
assert response.status_code == 404
Making Requests
GET Requests
def test_get_user(client):
"""Test getting user details."""
response = client.get('/users/1')
assert response.status_code == 200
# With query parameters
response = client.get('/search?q=flask&page=2')
assert response.status_code == 200
POST Requests
def test_create_user(client):
"""Test creating a user."""
response = client.post('/users', data={
'username': 'alice',
'email': '[email protected]'
})
assert response.status_code == 201
def test_json_post(client):
"""Test POST with JSON data."""
response = client.post('/api/users', json={
'username': 'alice',
'email': '[email protected]'
})
assert response.status_code == 201
data = response.get_json()
assert data['username'] == 'alice'
Other HTTP Methods
def test_update_user(client):
"""Test PUT request."""
response = client.put('/users/1', json={
'email': '[email protected]'
})
assert response.status_code == 200
def test_delete_user(client):
"""Test DELETE request."""
response = client.delete('/users/1')
assert response.status_code == 204
def test_patch_user(client):
"""Test PATCH request."""
response = client.patch('/users/1', json={
'email': '[email protected]'
})
assert response.status_code == 200
Working with Sessions
Session Transactions
def test_session(client):
"""Test session manipulation."""
with client.session_transaction() as session:
session['user_id'] = 1
session['username'] = 'alice'
# Session is now set for subsequent requests
response = client.get('/dashboard')
assert response.status_code == 200
def test_login_session(client):
"""Test login creates session."""
client.post('/login', data={
'username': 'alice',
'password': 'secret'
})
with client.session_transaction() as session:
assert session['user_id'] == 1
assert 'username' in session
Testing JSON APIs
Parsing JSON Responses
def test_json_response(client):
"""Test JSON API response."""
response = client.get('/api/users/1')
assert response.status_code == 200
assert response.content_type == 'application/json'
data = response.get_json()
assert data['username'] == 'alice'
assert data['email'] == '[email protected]'
def test_json_list(client):
"""Test JSON list response."""
response = client.get('/api/users')
data = response.get_json()
assert isinstance(data, list)
assert len(data) > 0
assert all('username' in user for user in data)
Testing Error Responses
def test_api_error(client):
"""Test API error response."""
response = client.get('/api/users/999')
assert response.status_code == 404
data = response.get_json()
assert 'error' in data
assert data['error'] == 'User not found'
def test_validation_error(client):
"""Test validation error."""
response = client.post('/api/users', json={
'username': '' # Invalid
})
assert response.status_code == 400
data = response.get_json()
assert 'errors' in data
assert 'username' in data['errors']
File Uploads
Testing File Upload
from io import BytesIO
def test_file_upload(client):
"""Test file upload."""
data = {
'file': (BytesIO(b'file contents'), 'test.txt')
}
response = client.post(
'/upload',
data=data,
content_type='multipart/form-data'
)
assert response.status_code == 200
def test_image_upload(client):
"""Test image upload."""
with open('tests/files/test.jpg', 'rb') as f:
data = {'image': (f, 'test.jpg')}
response = client.post(
'/upload-image',
data=data,
content_type='multipart/form-data'
)
assert response.status_code == 200
Headers and Cookies
Custom Headers
def test_with_headers(client):
"""Test request with custom headers."""
response = client.get('/api/users', headers={
'Authorization': 'Bearer token123',
'X-API-Key': 'secret-key'
})
assert response.status_code == 200
def test_content_type(client):
"""Test specific content type."""
response = client.post(
'/api/users',
data='{"username": "alice"}',
content_type='application/json'
)
assert response.status_code == 201
Cookies
def test_cookies(client):
"""Test cookie handling."""
client.set_cookie('session', 'cookie-value')
response = client.get('/dashboard')
assert response.status_code == 200
def test_response_cookies(client):
"""Test response sets cookies."""
response = client.post('/login', data={
'username': 'alice',
'password': 'secret'
})
assert 'session' in response.headers.getlist('Set-Cookie')[0]
Context Preservation
Preserving Context
def test_with_context(client):
"""Test with preserved context."""
with client:
response = client.get('/')
# Context is preserved, can access request, session, g
from flask import session
assert 'user_id' in session
def test_multiple_requests(client):
"""Test multiple requests with preserved context."""
with client:
client.post('/login', data={'username': 'alice'})
# Session persists
response = client.get('/dashboard')
assert response.status_code == 200
response = client.get('/profile')
assert response.status_code == 200
Testing CLI Commands
Basic CLI Testing
def test_init_db_command(runner):
"""Test init-db CLI command."""
result = runner.invoke(args=['init-db'])
assert result.exit_code == 0
assert 'Initialized' in result.output
def test_cli_with_args(runner):
"""Test CLI command with arguments."""
result = runner.invoke(args=['create-user', 'alice', '[email protected]'])
assert result.exit_code == 0
assert 'Created user alice' in result.output
Testing CLI Errors
def test_cli_error(runner):
"""Test CLI command error handling."""
result = runner.invoke(args=['invalid-command'])
assert result.exit_code != 0
assert 'Error' in result.output
def test_cli_missing_arg(runner):
"""Test CLI command with missing argument."""
result = runner.invoke(args=['create-user'])
assert result.exit_code != 0
Testing Configuration
Test Configuration
class TestConfig:
"""Configuration for tests."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
SECRET_KEY = 'test-secret-key'
@pytest.fixture
def app():
app = create_app()
app.config.from_object(TestConfig)
return app
Environment-Specific Tests
def test_production_config():
"""Test production configuration."""
app = create_app('production')
assert not app.config['DEBUG']
assert not app.config['TESTING']
def test_development_config():
"""Test development configuration."""
app = create_app('development')
assert app.config['DEBUG']
Database Testing
Using In-Memory Database
@pytest.fixture
def app():
app = create_app()
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
with app.app_context():
db.create_all()
yield app
db.drop_all()
Test Data Fixtures
@pytest.fixture
def user(app):
"""Create a test user."""
user = User(username='alice', email='[email protected]')
db.session.add(user)
db.session.commit()
return user
def test_with_user(client, user):
"""Test using user fixture."""
response = client.get(f'/users/{user.id}')
assert response.status_code == 200
Mocking and Patching
Mocking External Services
from unittest.mock import patch, MagicMock
def test_external_api(client):
"""Test with mocked external API."""
with patch('myapp.services.external_api') as mock_api:
mock_api.return_value = {'status': 'success'}
response = client.post('/process')
assert response.status_code == 200
mock_api.assert_called_once()
def test_send_email(client):
"""Test with mocked email sending."""
with patch('myapp.email.send_email') as mock_send:
response = client.post('/contact', data={
'email': '[email protected]',
'message': 'Hello'
})
assert response.status_code == 200
mock_send.assert_called_once()
Best Practices
1. Use Fixtures for Common Setup
@pytest.fixture
def authenticated_client(client):
"""Client with authenticated user."""
client.post('/login', data={
'username': 'alice',
'password': 'secret'
})
return client
def test_protected_route(authenticated_client):
response = authenticated_client.get('/dashboard')
assert response.status_code == 200
2. Test Edge Cases
def test_empty_username(client):
"""Test with empty username."""
response = client.post('/users', json={'username': ''})
assert response.status_code == 400
def test_duplicate_email(client):
"""Test duplicate email error."""
client.post('/users', json={'email': '[email protected]'})
response = client.post('/users', json={'email': '[email protected]'})
assert response.status_code == 400
3. Test Security
def test_csrf_protection(client):
"""Test CSRF protection."""
response = client.post('/delete-account')
assert response.status_code == 400
def test_unauthorized_access(client):
"""Test accessing protected route without auth."""
response = client.get('/admin')
assert response.status_code == 401
4. Use Parametrized Tests
import pytest
@pytest.mark.parametrize('path,status_code', [
('/', 200),
('/about', 200),
('/contact', 200),
('/nonexistent', 404),
])
def test_routes(client, path, status_code):
"""Test multiple routes."""
response = client.get(path)
assert response.status_code == status_code
5. Test Cleanup
@pytest.fixture
def temp_file(tmp_path):
"""Create temporary file for testing."""
file_path = tmp_path / 'test.txt'
file_path.write_text('test content')
yield file_path
# Cleanup happens automatically with tmp_path
Coverage
Running with Coverage
pip install pytest-cov
pytest --cov=myapp tests/
pytest --cov=myapp --cov-report=html tests/
Coverage Configuration
# .coveragerc
[run]
source = myapp
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
