Test Client Basics

Difficulty: Advanced

Flask provides a built-in test client that lets you make HTTP requests to your application without running a real server. This is the foundation of testing Flask applications. The test client simulates requests, and you assert on the responses to verify your application behaves correctly.

You create a test client from your Flask application with app.test_client(). The client provides methods for every HTTP verb: client.get(), client.post(), client.put(), client.delete(), client.patch(). These methods accept the URL path, data (for form data), json (for JSON body), headers, and other parameters.

The response object from the test client contains status_code, data (raw bytes), json (parsed JSON), headers, and other properties. You use Python assertions to verify the response matches your expectations.

With the application factory pattern, you create a fresh app with test configuration for each test or test session. Test configuration typically uses an in-memory SQLite database, disables CSRF protection, and sets TESTING=True. The TESTING flag disables error catching, so exceptions propagate to the test instead of returning 500.

Flask's test client does not send actual HTTP requests over the network. It calls the WSGI application directly, making tests fast and reliable. There is no need to start a server, deal with port conflicts, or handle network issues.

The test client maintains cookies between requests within the same client instance, simulating a real browser session. This lets you test login flows: POST to /login, then GET a protected page and verify access.

For testing with application context, use app.test_request_context() to push a fake request context. Inside this context, you can use request, current_app, and other context-dependent objects. Use app.app_context() when you only need the application context (for database operations without a request).

Code examples

Basic Test Client Usage

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello, World!'

@app.route('/api/status')
def status():
    return {'status': 'ok', 'version': '1.0'}

# Testing
def test_index():
    client = app.test_client()
    response = client.get('/')
    assert response.status_code == 200
    assert b'Hello, World!' in response.data

def test_status():
    client = app.test_client()
    response = client.get('/api/status')
    assert response.status_code == 200
    data = response.get_json()
    assert data['status'] == 'ok'
    assert data['version'] == '1.0'

def test_not_found():
    client = app.test_client()
    response = client.get('/nonexistent')
    assert response.status_code == 404

Create a test client with app.test_client(). Make requests with client.get(), client.post(), etc. Assert on status_code, data (bytes), and get_json() (parsed JSON).

Testing POST Requests

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post('/api/users')
def create_user():
    data = request.get_json()
    if not data or 'name' not in data:
        return jsonify({'error': 'Name required'}), 400
    return jsonify({'id': 1, 'name': data['name']}), 201

# Testing
def test_create_user_success():
    client = app.test_client()
    response = client.post('/api/users',
        json={'name': 'Alice'},
        content_type='application/json'
    )
    assert response.status_code == 201
    assert response.get_json()['name'] == 'Alice'

def test_create_user_missing_name():
    client = app.test_client()
    response = client.post('/api/users',
        json={},
        content_type='application/json'
    )
    assert response.status_code == 400
    assert 'error' in response.get_json()

def test_create_user_form_data():
    client = app.test_client()
    response = client.post('/api/users',
        data={'name': 'Bob'}  # Form data instead of JSON
    )
    # This would fail because the view expects JSON
    assert response.status_code == 400

Use json= parameter for JSON requests and data= for form data. Test both success and error cases. Verify status codes and response body content.

Testing with Authentication

from flask import Flask, session, jsonify

app = Flask(__name__)
app.config['SECRET_KEY'] = 'test-secret'

@app.post('/login')
def login():
    data = request.get_json()
    if data.get('password') == 'secret':
        session['user'] = data['username']
        return jsonify({'message': 'Logged in'})
    return jsonify({'error': 'Invalid'}), 401

@app.get('/protected')
def protected():
    if 'user' not in session:
        return jsonify({'error': 'Unauthorized'}), 401
    return jsonify({'message': f'Hello, {session["user"]}'})

# Testing
def test_login_and_access():
    client = app.test_client()

    # Login
    response = client.post('/login', json={
        'username': 'alice', 'password': 'secret'
    })
    assert response.status_code == 200

    # Access protected route (session cookie is maintained)
    response = client.get('/protected')
    assert response.status_code == 200
    assert response.get_json()['message'] == 'Hello, alice'

def test_protected_without_login():
    client = app.test_client()
    response = client.get('/protected')
    assert response.status_code == 401

The test client maintains cookies between requests. After logging in, subsequent requests include the session cookie. Create a new client instance for a fresh session.

Testing with Custom Headers

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.get('/api/data')
def get_data():
    auth = request.headers.get('Authorization', '')
    if not auth.startswith('Bearer '):
        return jsonify({'error': 'Unauthorized'}), 401

    token = auth[7:]
    if token != 'valid-token':
        return jsonify({'error': 'Invalid token'}), 401

    return jsonify({'data': 'secret info'})

# Testing
def test_with_valid_token():
    client = app.test_client()
    response = client.get('/api/data',
        headers={'Authorization': 'Bearer valid-token'}
    )
    assert response.status_code == 200
    assert response.get_json()['data'] == 'secret info'

def test_without_token():
    client = app.test_client()
    response = client.get('/api/data')
    assert response.status_code == 401

def test_with_invalid_token():
    client = app.test_client()
    response = client.get('/api/data',
        headers={'Authorization': 'Bearer wrong-token'}
    )
    assert response.status_code == 401

Pass custom headers via the headers dict. Test all authentication scenarios: valid token, missing token, and invalid token. This pattern works for Bearer tokens, API keys, and other header-based auth.

Key points

Concepts covered

Test Client, app.test_client, Test Requests, Response Assertions, Test Context