Testing Stack
The project uses the following testing dependencies fromrequirements/testing.txt:
pytest==8.3.4 # Testing framework
pytest-flask==1.3.0 # Flask-specific fixtures
pytest-mock==3.14.0 # Mocking utilities
pytest-cov==5.0.0 # Coverage reporting
pytest-xdist==3.6.1 # Parallel test execution
factory-boy==3.5.0 # Test data factories
freezegun==1.6.0 # Time mocking
Faker==30.4.0 # Fake data generation
coverage==7.6.8 # Coverage analysis
flake8==7.1.2 # Code linting
bandit==1.8.2 # Security testing
safety==2.6.0 # Dependency security
codecov==2.1.15 # Coverage reporting
Test Structure
Tests are organized by type in thetests/ directory:
tests/
├── unit/ # Unit tests
│ ├── domain/ # Domain entity tests
│ ├── application/ # Use case tests
│ └── infrastructure/ # Repository tests
├── integration/ # Integration tests
│ ├── api/ # API endpoint tests
│ └── database/ # Database integration tests
├── fixtures/ # Shared test fixtures
│ ├── factories.py # Factory Boy factories
│ └── conftest.py # Pytest fixtures
└── conftest.py # Root configuration
Setting Up Tests
Install Testing Dependencies
Install all testing dependencies:
pip install -r requirements/testing.txt
Configure Test Database
Set up a separate database for testing in your Create the test database:
.env file:FLASK_ENV=testing
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/softbee_test
psql -U postgres
CREATE DATABASE softbee_test;
\q
Create Test Configuration
The testing configuration is defined in
config.py:79-88:class TestingConfig(Config):
"""Configuration for tests"""
DEBUG = True
TESTING = True
# PostgreSQL test database
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql://postgres:postgres@localhost:5432/softbee_test"
)
# Disable protections for easier testing
WTF_CSRF_ENABLED = False
Writing Tests
Unit Tests - Domain Entities
Test business logic in isolation:# tests/unit/domain/test_user_entity.py
import pytest
from datetime import datetime
from src.features.auth.domain.entities.user import User
from src.features.auth.domain.value_objects.email import Email
from src.features.auth.domain.exceptions.auth_exceptions import InvalidUserException
class TestUserEntity:
"""Test User domain entity"""
def test_create_valid_user(self):
"""Test creating a valid user"""
user = User(
email=Email("[email protected]"),
username="testuser",
hashed_password="hashed_password_123"
)
assert user.email.value == "[email protected]"
assert user.username == "testuser"
assert user.is_active is True
assert user.is_verified is False
def test_username_too_short(self):
"""Test username validation"""
with pytest.raises(InvalidUserException, match="at least 3 characters"):
User(
email=Email("[email protected]"),
username="ab", # Too short
hashed_password="hashed_password_123"
)
def test_invalid_email(self):
"""Test email validation"""
with pytest.raises(ValueError, match="Invalid email"):
User(
email=Email("invalid-email"),
username="testuser",
hashed_password="hashed_password_123"
)
def test_login_successful(self):
"""Test successful login updates state"""
user = User(
email=Email("[email protected]"),
username="testuser",
hashed_password="hashed_password_123"
)
user.failed_login_attempts = 2
user.login_successful()
assert user.failed_login_attempts == 0
assert user.last_login is not None
assert len(user.pull_events()) > 0 # Event was registered
def test_account_lockout(self):
"""Test account locks after failed attempts"""
user = User(
email=Email("[email protected]"),
username="testuser",
hashed_password="hashed_password_123"
)
for _ in range(5):
user.login_failed()
assert user.is_locked() is True
Unit Tests - Use Cases
Test application logic with mocked dependencies:# tests/unit/application/test_register_user_use_case.py
import pytest
from unittest.mock import Mock, MagicMock
from src.features.auth.application.use_cases.register_user import RegisterUserUseCase
from src.features.auth.application.dto.auth_dto import RegisterRequestDTO
from src.features.auth.domain.entities.user import User
from src.features.auth.domain.value_objects.email import Email
class TestRegisterUserUseCase:
"""Test user registration use case"""
@pytest.fixture
def mock_repository(self):
"""Create mock user repository"""
repository = Mock()
repository.exists_by_email.return_value = False
repository.exists_by_username.return_value = False
return repository
@pytest.fixture
def mock_hasher(self):
"""Create mock password hasher"""
hasher = Mock()
hasher.hash.return_value = "hashed_password_123"
return hasher
@pytest.fixture
def use_case(self, mock_repository, mock_hasher):
"""Create use case instance"""
return RegisterUserUseCase(
user_repository=mock_repository,
password_hasher=mock_hasher
)
def test_successful_registration(self, use_case, mock_repository):
"""Test successful user registration"""
# Arrange
request = RegisterRequestDTO(
email="[email protected]",
username="testuser",
password="password123"
)
saved_user = User(
id="user_123",
email=Email("[email protected]"),
username="testuser",
hashed_password="hashed_password_123"
)
mock_repository.save.return_value = saved_user
# Act
result, error = use_case.execute(request)
# Assert
assert error is None
assert result is not None
assert result.id == "user_123"
assert result.email == "[email protected]"
assert result.username == "testuser"
mock_repository.save.assert_called_once()
def test_duplicate_email(self, use_case, mock_repository):
"""Test registration with existing email"""
# Arrange
mock_repository.exists_by_email.return_value = True
request = RegisterRequestDTO(
email="[email protected]",
username="testuser",
password="password123"
)
# Act
result, error = use_case.execute(request)
# Assert
assert result is None
assert "already exists" in error.lower()
mock_repository.save.assert_not_called()
Integration Tests - API Endpoints
Test complete request/response flows:# tests/integration/api/test_auth_endpoints.py
import pytest
import json
from app import create_app
from src.core.database.db import db
@pytest.fixture
def app():
"""Create test Flask application"""
app = create_app(testing=True)
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client"""
return app.test_client()
class TestAuthEndpoints:
"""Test authentication API endpoints"""
def test_register_success(self, client):
"""Test successful registration"""
response = client.post(
'/api/v1/auth/register',
data=json.dumps({
'email': '[email protected]',
'username': 'newuser',
'password': 'SecurePassword123!'
}),
content_type='application/json'
)
assert response.status_code == 201
data = json.loads(response.data)
assert data['email'] == '[email protected]'
assert data['username'] == 'newuser'
assert 'id' in data
def test_register_duplicate_email(self, client):
"""Test registration with duplicate email"""
# First registration
client.post(
'/api/v1/auth/register',
data=json.dumps({
'email': '[email protected]',
'username': 'user1',
'password': 'Password123!'
}),
content_type='application/json'
)
# Second registration with same email
response = client.post(
'/api/v1/auth/register',
data=json.dumps({
'email': '[email protected]',
'username': 'user2',
'password': 'Password123!'
}),
content_type='application/json'
)
assert response.status_code == 400
data = json.loads(response.data)
assert 'error' in data
def test_login_success(self, client):
"""Test successful login"""
# Register user first
client.post(
'/api/v1/auth/register',
data=json.dumps({
'email': '[email protected]',
'username': 'loginuser',
'password': 'Password123!'
}),
content_type='application/json'
)
# Login
response = client.post(
'/api/v1/auth/login',
data=json.dumps({
'email': '[email protected]',
'password': 'Password123!'
}),
content_type='application/json'
)
assert response.status_code == 200
data = json.loads(response.data)
assert 'access_token' in data
assert 'refresh_token' in data
def test_health_endpoint(self, client):
"""Test auth health endpoint"""
response = client.get('/api/v1/auth/health')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'healthy'
assert data['feature'] == 'auth'
Using Test Factories
Use Factory Boy to create test data:# tests/fixtures/factories.py
import factory
from factory import Factory, Faker
from src.features.auth.domain.entities.user import User
from src.features.auth.domain.value_objects.email import Email
class UserFactory(Factory):
class Meta:
model = User
email = factory.LazyAttribute(lambda _: Email(Faker().email()))
username = Faker('user_name')
hashed_password = 'hashed_password_123'
is_active = True
is_verified = False
# Usage in tests
def test_with_factory():
user = UserFactory.create()
assert user.is_active is True
Running Tests
Run with Coverage
Generate coverage report:View the HTML report:
pytest --cov=src --cov-report=html
open htmlcov/index.html
Test Best Practices
Follow AAA Pattern
Follow AAA Pattern
Structure tests with Arrange, Act, Assert:
def test_example():
# Arrange - Set up test data
user = UserFactory.create()
# Act - Execute the operation
result = user.login_successful()
# Assert - Verify the outcome
assert user.last_login is not None
Use Descriptive Test Names
Use Descriptive Test Names
Test names should describe what they test:
# Good
def test_user_cannot_login_with_invalid_password()
def test_registration_fails_when_email_already_exists()
# Bad
def test_login()
def test_user()
Mock External Dependencies
Mock External Dependencies
Never call real external services in tests:
# Good - Mock the email service
@patch('src.infrastructure.services.email_service.EmailService')
def test_sends_email(mock_email):
mock_email.send.return_value = True
# test logic
# Bad - Calls real SMTP server
def test_sends_email():
email_service = EmailService()
email_service.send(...) # Don't do this!
Test Edge Cases
Test Edge Cases
Test boundary conditions and error scenarios:
def test_username_minimum_length()
def test_username_maximum_length()
def test_username_with_special_characters()
def test_username_empty_string()
def test_username_none_value()
Keep Tests Independent
Keep Tests Independent
Each test should be able to run in isolation:
# Good - Each test creates its own data
def test_a():
user = UserFactory.create()
# test logic
def test_b():
user = UserFactory.create()
# test logic
# Bad - Tests depend on order
user = None
def test_a():
global user
user = UserFactory.create()
def test_b():
# Depends on test_a running first!
assert user is not None
Code Quality Tools
Linting with Flake8
flake8 src/ tests/
Security Scanning with Bandit
bandit -r src/
Dependency Security with Safety
safety check
Continuous Integration
Example GitHub Actions workflow:name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements/testing.txt
- name: Run tests
run: |
pytest --cov=src --cov-report=xml
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Upload coverage
uses: codecov/codecov-action@v3
Next Steps
Migrations
Learn about database migrations
Creating Features
Build new features with tests
Setup Guide
Complete development setup
Architecture
Learn about Clean Architecture