Skip to main content

Testing

Flask provides excellent testing capabilities through its built-in test client and integration with popular testing frameworks like pytest and unittest.

Setup and Configuration

Installation

pip install pytest
pip install pytest-flask
pip install pytest-cov # For coverage reports

Test Configuration

# config.py
class TestingConfig:
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'test-secret-key'
WTF_CSRF_ENABLED = False
LOGIN_DISABLED = True # Disable login for testing

# conftest.py
import pytest
from app import create_app, db
from app.models import User, Post

@pytest.fixture
def app():
app = create_app('testing')

with app.app_context():
db.create_all()
yield app
db.drop_all()

@pytest.fixture
def client(app):
return app.test_client()

@pytest.fixture
def runner(app):
return app.test_cli_runner()

Basic Testing

Simple Route Testing

# test_routes.py
def test_index(client):
response = client.get('/')
assert response.status_code == 200
assert b'Welcome' in response.data

def test_about_page(client):
response = client.get('/about')
assert response.status_code == 200
assert 'About' in response.get_data(as_text=True)

def test_404_error(client):
response = client.get('/nonexistent')
assert response.status_code == 404

Testing with JSON

def test_api_get_users(client):
response = client.get('/api/users')
assert response.status_code == 200

data = response.get_json()
assert 'users' in data
assert isinstance(data['users'], list)

def test_api_create_user(client):
user_data = {
'username': 'testuser',
'email': 'test@example.com'
}

response = client.post('/api/users',
json=user_data,
headers={'Content-Type': 'application/json'})

assert response.status_code == 201
data = response.get_json()
assert data['username'] == 'testuser'

Database Testing

Database Fixtures

# conftest.py
@pytest.fixture
def user():
user = User(username='testuser', email='test@example.com')
user.set_password('password')
db.session.add(user)
db.session.commit()
return user

@pytest.fixture
def users():
users = [
User(username='alice', email='alice@example.com'),
User(username='bob', email='bob@example.com'),
User(username='charlie', email='charlie@example.com')
]
for user in users:
user.set_password('password')
db.session.add(user)
db.session.commit()
return users

@pytest.fixture
def post(user):
post = Post(
title='Test Post',
content='This is a test post.',
author=user
)
db.session.add(post)
db.session.commit()
return post

Model Testing

# test_models.py
from app.models import User, Post

def test_user_creation():
user = User(username='testuser', email='test@example.com')
assert user.username == 'testuser'
assert user.email == 'test@example.com'

def test_password_hashing():
user = User(username='testuser', email='test@example.com')
user.set_password('password')

assert user.password_hash != 'password'
assert user.check_password('password') == True
assert user.check_password('wrongpassword') == False

def test_user_repr():
user = User(username='testuser')
assert repr(user) == '<User testuser>'

def test_post_relationship(user):
post = Post(title='Test Post', content='Content', author=user)
db.session.add(post)
db.session.commit()

assert post.author == user
assert post in user.posts

Authentication Testing

Login/Logout Testing

# test_auth.py
def test_login_page(client):
response = client.get('/auth/login')
assert response.status_code == 200
assert b'Login' in response.data

def test_valid_login(client, user):
response = client.post('/auth/login', data={
'username': 'testuser',
'password': 'password'
}, follow_redirects=True)

assert response.status_code == 200
assert b'Dashboard' in response.data

def test_invalid_login(client, user):
response = client.post('/auth/login', data={
'username': 'testuser',
'password': 'wrongpassword'
})

assert b'Invalid username or password' in response.data

def test_logout(client, user):
# Login first
client.post('/auth/login', data={
'username': 'testuser',
'password': 'password'
})

# Then logout
response = client.get('/auth/logout', follow_redirects=True)
assert response.status_code == 200
assert b'Login' in response.data

Protected Route Testing

def test_protected_route_without_login(client):
response = client.get('/dashboard')
assert response.status_code == 302 # Redirect to login

def test_protected_route_with_login(client, user):
# Login first
client.post('/auth/login', data={
'username': 'testuser',
'password': 'password'
})

# Access protected route
response = client.get('/dashboard')
assert response.status_code == 200
assert b'Welcome' in response.data

Form Testing

Form Validation Testing

# test_forms.py
from app.forms import RegistrationForm, LoginForm

def test_registration_form_valid():
form_data = {
'username': 'testuser',
'email': 'test@example.com',
'password': 'password123',
'confirm_password': 'password123'
}

form = RegistrationForm(data=form_data)
assert form.validate() == True

def test_registration_form_invalid_email():
form_data = {
'username': 'testuser',
'email': 'invalid-email',
'password': 'password123',
'confirm_password': 'password123'
}

form = RegistrationForm(data=form_data)
assert form.validate() == False
assert 'Invalid email address' in form.email.errors

def test_registration_form_password_mismatch():
form_data = {
'username': 'testuser',
'email': 'test@example.com',
'password': 'password123',
'confirm_password': 'different'
}

form = RegistrationForm(data=form_data)
assert form.validate() == False
assert 'Passwords must match' in form.confirm_password.errors

Form Submission Testing

def test_registration_form_submission(client):
form_data = {
'username': 'newuser',
'email': 'new@example.com',
'password': 'password123',
'confirm_password': 'password123'
}

response = client.post('/auth/register', data=form_data, follow_redirects=True)
assert response.status_code == 200
assert b'Registration successful' in response.data

# Check user was created
user = User.query.filter_by(username='newuser').first()
assert user is not None
assert user.email == 'new@example.com'

File Upload Testing

Testing File Uploads

import io

def test_file_upload_valid(client):
data = {
'file': (io.BytesIO(b'fake image data'), 'test.jpg')
}

response = client.post('/upload',
data=data,
content_type='multipart/form-data')

assert response.status_code == 200
assert b'File uploaded successfully' in response.data

def test_file_upload_invalid_type(client):
data = {
'file': (io.BytesIO(b'fake data'), 'test.txt')
}

response = client.post('/upload',
data=data,
content_type='multipart/form-data')

assert response.status_code == 400
assert b'Invalid file type' in response.data

API Testing

RESTful API Testing

# test_api.py
def test_get_users_empty(client):
response = client.get('/api/users')
assert response.status_code == 200

data = response.get_json()
assert data['users'] == []
assert data['total'] == 0

def test_create_user_success(client):
user_data = {
'username': 'apiuser',
'email': 'api@example.com'
}

response = client.post('/api/users',
json=user_data,
headers={'Content-Type': 'application/json'})

assert response.status_code == 201
data = response.get_json()
assert data['username'] == 'apiuser'
assert 'id' in data

def test_create_user_missing_data(client):
response = client.post('/api/users',
json={},
headers={'Content-Type': 'application/json'})

assert response.status_code == 400
data = response.get_json()
assert 'error' in data

def test_get_user_by_id(client, user):
response = client.get(f'/api/users/{user.id}')
assert response.status_code == 200

data = response.get_json()
assert data['username'] == user.username
assert data['id'] == user.id

def test_update_user(client, user):
update_data = {'username': 'updateduser'}

response = client.put(f'/api/users/{user.id}',
json=update_data,
headers={'Content-Type': 'application/json'})

assert response.status_code == 200
data = response.get_json()
assert data['username'] == 'updateduser'

def test_delete_user(client, user):
response = client.delete(f'/api/users/{user.id}')
assert response.status_code == 204

# Verify user is deleted
deleted_user = User.query.get(user.id)
assert deleted_user is None

Error Handling Testing

Error Page Testing

def test_404_error(client):
response = client.get('/nonexistent-page')
assert response.status_code == 404
assert b'Page Not Found' in response.data

def test_500_error(client, monkeypatch):
# Mock a function to raise an exception
def mock_error():
raise Exception("Test error")

monkeypatch.setattr('app.routes.some_function', mock_error)

response = client.get('/route-that-calls-some-function')
assert response.status_code == 500

Email Testing

Testing Email Functionality

# test_email.py
from flask_mail import Mail

def test_send_welcome_email(client, user):
with client.application.app_context():
with mail.record_messages() as outbox:
send_welcome_email(user)

assert len(outbox) == 1
assert outbox[0].subject == 'Welcome!'
assert user.email in outbox[0].recipients
assert 'Welcome to our app' in outbox[0].body

def test_password_reset_email(client, user):
with client.application.app_context():
with mail.record_messages() as outbox:
send_password_reset_email(user)

assert len(outbox) == 1
assert 'Password Reset' in outbox[0].subject
assert 'reset your password' in outbox[0].body

Mock Testing

Mocking External Services

from unittest.mock import patch, MagicMock

def test_external_api_call(client):
with patch('requests.get') as mock_get:
mock_response = MagicMock()
mock_response.json.return_value = {'data': 'test'}
mock_response.status_code = 200
mock_get.return_value = mock_response

response = client.get('/api/external-data')
assert response.status_code == 200

data = response.get_json()
assert data['data'] == 'test'

def test_database_error_handling(client):
with patch('app.db.session.commit') as mock_commit:
mock_commit.side_effect = Exception("Database error")

response = client.post('/api/users', json={
'username': 'testuser',
'email': 'test@example.com'
})

assert response.status_code == 500

Performance Testing

Load Testing Setup

def test_performance_user_creation(client):
import time

start_time = time.time()

for i in range(100):
user_data = {
'username': f'user{i}',
'email': f'user{i}@example.com'
}
response = client.post('/api/users', json=user_data)
assert response.status_code == 201

end_time = time.time()
duration = end_time - start_time

assert duration < 10 # Should complete in under 10 seconds
print(f"Created 100 users in {duration:.2f} seconds")

Test Utilities

Custom Test Helpers

# test_utils.py
import json

def login_user(client, username='testuser', password='password'):
"""Helper function to log in a user"""
return client.post('/auth/login', data={
'username': username,
'password': password
})

def create_test_user(username='testuser', email='test@example.com'):
"""Helper function to create a test user"""
user = User(username=username, email=email)
user.set_password('password')
db.session.add(user)
db.session.commit()
return user

def json_post(client, url, data):
"""Helper function for JSON POST requests"""
return client.post(url,
data=json.dumps(data),
headers={'Content-Type': 'application/json'})

# Usage in tests
def test_protected_route_with_helper(client):
user = create_test_user()
login_user(client)

response = client.get('/dashboard')
assert response.status_code == 200

Coverage and Reporting

Coverage Configuration

# pytest.ini
[tool:pytest]
addopts = --cov=app --cov-report=html --cov-report=term-missing
testpaths = tests
python_files = test_*.py
python_functions = test_*
python_classes = Test*

Running Tests with Coverage

# Run tests with coverage
pytest --cov=app

# Generate HTML coverage report
pytest --cov=app --cov-report=html

# Run specific test file
pytest tests/test_auth.py

# Run tests with verbose output
pytest -v

# Run tests and stop on first failure
pytest -x

# Run tests matching pattern
pytest -k "test_user"

Integration Testing

Full Application Testing

# test_integration.py
def test_user_registration_and_login_flow(client):
# Register a new user
registration_data = {
'username': 'integrationuser',
'email': 'integration@example.com',
'password': 'password123',
'confirm_password': 'password123'
}

response = client.post('/auth/register',
data=registration_data,
follow_redirects=True)
assert b'Registration successful' in response.data

# Login with the new user
login_data = {
'username': 'integrationuser',
'password': 'password123'
}

response = client.post('/auth/login',
data=login_data,
follow_redirects=True)
assert b'Dashboard' in response.data

# Access protected route
response = client.get('/profile')
assert response.status_code == 200
assert b'integrationuser' in response.data

def test_blog_post_crud_flow(client, user):
# Login
login_user(client)

# Create a post
post_data = {
'title': 'Test Post',
'content': 'This is test content for the post.'
}

response = client.post('/blog/create', data=post_data, follow_redirects=True)
assert b'Post created successfully' in response.data

# View the post
post = Post.query.filter_by(title='Test Post').first()
response = client.get(f'/blog/post/{post.id}')
assert response.status_code == 200
assert b'Test Post' in response.data

# Edit the post
edit_data = {
'title': 'Updated Test Post',
'content': 'Updated content.'
}

response = client.post(f'/blog/edit/{post.id}',
data=edit_data,
follow_redirects=True)
assert b'Post updated successfully' in response.data

# Delete the post
response = client.post(f'/blog/delete/{post.id}', follow_redirects=True)
assert b'Post deleted successfully' in response.data

# Verify post is deleted
deleted_post = Post.query.get(post.id)
assert deleted_post is None

Best Practices

Testing Guidelines

# 1. Use descriptive test names
def test_user_cannot_login_with_invalid_password():
pass

def test_api_returns_404_for_nonexistent_user():
pass

# 2. Test both positive and negative cases
def test_valid_registration():
pass

def test_registration_with_duplicate_username():
pass

# 3. Use fixtures for setup and teardown
@pytest.fixture
def authenticated_user(client):
user = create_test_user()
login_user(client, user.username)
return user

# 4. Keep tests independent
def test_user_creation():
# Don't rely on other tests
pass

# 5. Test error conditions
def test_database_connection_error():
with patch('app.db.session.commit') as mock_commit:
mock_commit.side_effect = SQLAlchemyError()
# Test error handling
pass

# 6. Use parametrized tests for multiple scenarios
@pytest.mark.parametrize("username,email,expected", [
("valid", "valid@example.com", True),
("", "valid@example.com", False),
("valid", "invalid-email", False),
])
def test_user_validation(username, email, expected):
form = RegistrationForm(data={
'username': username,
'email': email,
'password': 'password',
'confirm_password': 'password'
})
assert form.validate() == expected