Flask Unit Tests
Unit tests verify individual pieces of your Flask application in isolation — a model method, a helper function, or a single route. Unlike integration tests that test multiple layers together, unit tests focus on one function at a time and mock everything else.
What to Unit Test in Flask
- Model methods and properties (
user.full_name,product.apply_discount()) - Utility functions (password validators, URL builders)
- Form validation logic
- Individual API endpoints
- Business logic functions that don't touch the database directly
Testing Model Methods
# tests/test_models.py
def test_user_full_name(app):
from app.models import User
with app.app_context():
user = User(first_name='Alice', last_name='Smith')
assert user.full_name == 'Alice Smith'
def test_password_hashing(app):
from app.models import User
with app.app_context():
user = User(username='alice')
user.set_password('secret123')
assert user.check_password('secret123') is True
assert user.check_password('wrongpass') is False
def test_product_discount(app):
from app.models import Product
with app.app_context():
product = Product(name='Laptop', price=1000.0)
product.apply_discount(10) # 10% off
assert product.price == 900.0Testing with a Database
Use the in-memory SQLite database for tests — it is fast and disappears after each test run:
# tests/conftest.py
@pytest.fixture
def app():
app = create_app('testing') # SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
# Test with database operations
def test_create_and_read_user(app):
from app.models import User
from app import db
with app.app_context():
user = User(username='bob', email='bob@example.com')
user.set_password('password')
db.session.add(user)
db.session.commit()
found = User.query.filter_by(username='bob').first()
assert found is not None
assert found.email == 'bob@example.com'Mocking External Services
Unit tests should not send real emails, make real API calls, or charge real credit cards. Use unittest.mock to replace external calls with fakes:
from unittest.mock import patch, MagicMock
def test_registration_sends_email(client):
with patch('app.auth.routes.send_welcome_email') as mock_send:
response = client.post('/auth/register', data={
'username': 'alice',
'email': 'alice@example.com',
'password': 'StrongPass99'
})
assert response.status_code == 302
mock_send.assert_called_once() # email function was calledpatch() replaces the real send_welcome_email function with a mock that does nothing but records calls. The test verifies the function was called without actually sending an email.
Testing with Authenticated Users
Many routes require login. Simulate a logged-in user by manipulating the session:
def test_dashboard_requires_login(client):
response = client.get('/dashboard')
assert response.status_code == 302 # redirects to login
def test_dashboard_accessible_when_logged_in(client, app):
from app.models import User
from app import db
with app.app_context():
user = User(username='testuser', email='test@test.com')
user.set_password('pass')
db.session.add(user)
db.session.commit()
user_id = user.id
# Log in by setting the session
with client.session_transaction() as sess:
sess['user_id'] = user_id
response = client.get('/dashboard')
assert response.status_code == 200Parametrized Tests
Run the same test with multiple inputs using @pytest.mark.parametrize:
import pytest
@pytest.mark.parametrize('password,expected', [
('abc', False), # too short
('password', False), # no uppercase
('Password', False), # no number
('Password1', True), # valid
('Str0ngPass!', True), # valid
])
def test_password_strength(client, password, expected):
from app.utils import is_strong_password
assert is_strong_password(password) == expectedTest Coverage
Measure how much of your code is covered by tests:
pip install pytest-cov
pytest --cov=app --cov-report=htmlThis generates a coverage report in HTML showing which lines of code are tested (green) and which are not (red). Aim for at least 80% coverage on critical business logic.
Summary
Unit tests verify individual functions and methods in isolation. Test model methods, utility functions, and routes with clean database state per test. Use unittest.mock.patch to replace external services like email or payment APIs. Use session_transaction() to simulate authenticated requests. Parametrize tests to cover multiple inputs without duplicating test functions. Track coverage with pytest-cov to identify untested code paths.
