Skip to main content
The Soft-Bee API uses pytest as its testing framework, with specialized tools for Flask testing, mocking, and test coverage.

Testing Stack

The project uses the following testing dependencies from requirements/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 the tests/ 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

1

Install Testing Dependencies

Install all testing dependencies:
pip install -r requirements/testing.txt
2

Configure Test Database

Set up a separate database for testing in your .env file:
FLASK_ENV=testing
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/softbee_test
Create the test database:
psql -U postgres
CREATE DATABASE softbee_test;
\q
3

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

1

Run All Tests

Execute the entire test suite:
pytest
2

Run Specific Test File

Run tests in a specific file:
pytest tests/unit/domain/test_user_entity.py
3

Run Tests by Pattern

Run tests matching a pattern:
pytest -k "test_login"
4

Run with Coverage

Generate coverage report:
pytest --cov=src --cov-report=html
View the HTML report:
open htmlcov/index.html
5

Run Tests in Parallel

Use pytest-xdist for faster execution:
pytest -n auto
6

Run with Verbose Output

Show detailed test information:
pytest -v

Test Best Practices

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
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()
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 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()
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

Build docs developers (and LLMs) love