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.0

Testing 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 called

patch() 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 == 200

Parametrized 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) == expected

Test Coverage

Measure how much of your code is covered by tests:

pip install pytest-cov

pytest --cov=app --cov-report=html

This 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.

Leave a Comment

Your email address will not be published. Required fields are marked *