Skip to main content

Overview

Flask provides excellent support for testing applications through the FlaskClient 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__.:

Build docs developers (and LLMs) love