Test Organization#
FairDM follows a standardized directory structure and naming convention to make test discovery intuitive and consistent.
Directory Structure#
Tests are organized by layer and Django app to mirror the source code structure:
tests/
├── conftest.py # Root-level fixtures (database setup)
├── unit/ # Unit test layer
│ ├── conftest.py # Unit-specific fixtures (RequestFactory)
│ ├── fairdm/ # Mirror Django app structure
│ │ ├── core/
│ │ │ ├── test_models.py
│ │ │ ├── test_views.py
│ │ │ └── test_utils.py
│ │ ├── contrib/
│ │ │ └── user/
│ │ │ └── test_models.py
│ │ └── plugins/
│ │ └── test_registry.py
│ └── fairdm_demo/
│ └── test_models.py
├── integration/ # Integration test layer
│ ├── conftest.py # Integration-specific fixtures
│ ├── fairdm/
│ │ └── core/
│ │ ├── test_project_workflow.py
│ │ └── test_dataset_workflow.py
│ └── fairdm_demo/
│ └── test_demo_workflow.py
├── contract/ # Contract test layer
│ ├── conftest.py # Contract-specific fixtures (API client)
│ ├── test_api_project_schema.py
│ └── test_api_dataset_schema.py
└── fixtures/ # Shared test data
├── __init__.py
├── factories.py # Factory-boy factories
└── pytest_fixtures.py # Pytest fixtures
Mapping Rules#
Rule 1: Layer-Based Organization#
Tests are grouped by test layer (unit, integration, contract) at the top level:
tests/unit/- Fast, isolated teststests/integration/- Database-backed teststests/contract/- API/schema validation tests
Why: Layer-based organization allows running specific test types independently:
poetry run pytest tests/unit/ # Fast feedback
poetry run pytest tests/integration/ # Database tests
poetry run pytest tests/contract/ # API validation
Rule 2: App-Mirroring Structure#
Within each layer, test directories mirror Django app structure:
Source code:
fairdm/
├── core/
│ ├── models.py
│ ├── views.py
│ └── utils.py
└── contrib/
└── user/
└── models.py
Test code:
tests/unit/fairdm/
├── core/
│ ├── test_models.py
│ ├── test_views.py
│ └── test_utils.py
└── contrib/
└── user/
└── test_models.py
Why: Mirroring makes it easy to find tests for specific modules.
Rule 3: Module-to-Test File Mapping#
Test files use the prefix test_ followed by the module name:
Source Module |
Test File |
|---|---|
|
|
|
|
|
|
|
|
Exception: Contract tests may not mirror source structure if testing API endpoints:
tests/contract/
├── test_api_project_schema.py # Tests /api/projects/
└── test_api_dataset_schema.py # Tests /api/datasets/
Conftest Files#
Pytest uses conftest.py files to provide fixtures at different scopes:
Root conftest.py#
Location: tests/conftest.py
Purpose: Session-level fixtures available to all tests
Example:
"""Root conftest for all test layers."""
import pytest
from django.core.management import call_command
@pytest.fixture(scope="session")
def django_db_setup(django_db_setup, django_db_blocker):
"""
Create test database once per session.
Uses django_db_blocker to ensure database is created before tests run.
"""
with django_db_blocker.unblock():
call_command("migrate", "--noinput")
Layer-Specific conftest.py#
Unit Layer (tests/unit/conftest.py):
"""Conftest for unit tests."""
import pytest
from django.test import RequestFactory
@pytest.fixture
def rf():
"""
RequestFactory instance for creating mock HTTP requests.
Used in unit tests to test views without full HTTP stack.
"""
return RequestFactory()
Integration Layer (tests/integration/conftest.py):
"""Conftest for integration tests."""
import pytest
# Integration-specific fixtures go here
# Example: fixtures for complex database scenarios
Contract Layer (tests/contract/conftest.py):
"""Conftest for contract tests."""
import pytest
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
"""
DRF APIClient for testing API endpoints.
Provides authentication and JSON handling.
"""
return APIClient()
App-Specific conftest.py#
You can also create conftest.py files at the app level:
Example (tests/unit/fairdm/core/conftest.py):
"""Fixtures specific to fairdm.core unit tests."""
import pytest
from fairdm.core.models import Project
@pytest.fixture
def sample_project_data():
"""
Sample data for Project creation in unit tests.
Returns dict without creating database objects.
"""
return {
"title": "Test Project",
"slug": "test-project",
"description": "A test project",
}
Test Naming Convention#
File Names#
Test files MUST use the pattern: test_<module>.py
Examples:
test_models.py- Tests for models.pytest_views.py- Tests for views.pytest_serializers.py- Tests for serializers.py
Test Function Names#
Test functions MUST use the pattern: test_<behavior>__<condition>__<expected>
Pattern Breakdown:
Component |
Description |
Example |
|---|---|---|
|
What is being tested |
|
|
Key context or state |
|
|
Observable outcome |
|
Examples:
# ✅ Clear and descriptive
def test_login__with_invalid_password__returns_error():
...
def test_project_creation__with_blank_title__raises_validation_error():
...
def test_dataset_delete__with_related_samples__cascades_deletion():
...
# ❌ Unclear intent
def test_login():
...
def test_project():
...
def test_1():
...
Docstrings#
Each test SHOULD include a docstring explaining why the behavior matters:
def test_project_visibility__private_project_with_anonymous_user__returns_403():
"""
Anonymous users cannot access private projects.
This ensures projects marked private remain inaccessible
until explicit permission is granted via django-guardian.
"""
...
Complete Example#
Here’s a complete example showing proper organization:
Source File: fairdm/core/models.py
class Project(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return f"/projects/{self.slug}/"
Unit Test: tests/unit/fairdm/core/test_models.py
"""Unit tests for fairdm.core.models."""
from fairdm.core.models import Project
def test_get_absolute_url__with_valid_slug__returns_detail_url():
"""Test URL generation without database access."""
project = Project(pk=123, slug="test-project")
url = project.get_absolute_url()
assert url == "/projects/test-project/"
Integration Test: tests/integration/fairdm/core/test_project_workflow.py
"""Integration tests for project creation workflow."""
import pytest
from fairdm.factories import ProjectFactory, UserFactory
@pytest.mark.django_db
def test_project_creation__with_valid_data__creates_project_and_assigns_owner():
"""Test complete project creation workflow."""
user = UserFactory()
project = ProjectFactory(owner=user, title="Test Project")
assert project.pk is not None
assert project.owner == user
Discovery Patterns#
Pytest uses these patterns to discover tests (configured in pyproject.toml):
[tool.pytest.ini_options]
python_files = ["test_*.py"] # Find test files
python_classes = ["Test*"] # Find test classes
python_functions = ["test_*"] # Find test functions
testpaths = ["tests"] # Search in tests/ directory
This means:
✅
test_models.py- Discovered✅
def test_something()- Discovered✅
class TestProject- Discovered❌
models_test.py- Not discovered❌
def check_something()- Not discovered
Best Practices#
1. One Test File Per Source Module#
Good:
fairdm/core/models.py → tests/unit/fairdm/core/test_models.py
fairdm/core/views.py → tests/unit/fairdm/core/test_views.py
Bad:
fairdm/core/models.py → tests/unit/fairdm/core/test_all.py
fairdm/core/views.py → tests/unit/fairdm/core/test_all.py
3. Use Markers for Test Selection#
Mark tests with pytest markers for selective execution:
import pytest
@pytest.mark.unit
def test_something_fast():
...
@pytest.mark.integration
@pytest.mark.django_db
def test_something_with_database():
...
@pytest.mark.slow
def test_something_slow():
...
Run specific markers:
poetry run pytest -m unit # Only unit tests
poetry run pytest -m integration # Only integration tests
poetry run pytest -m "not slow" # Exclude slow tests
Common Pitfalls#
Placing tests in wrong layer or not mirroring app structure.
Fix: Follow tests/{layer}/{app}/test_{module}.py pattern.
Using generic names like test_1 or test_project.
Fix: Use test_<behavior>__<condition>__<expected> pattern.
Duplicating fixtures across test files.
Fix: Create conftest.py at appropriate level with shared fixtures.
Testing API endpoints in unit test layer.
Fix: Move API/schema tests to tests/contract/.
Next Steps#
See also
Review Test Layers for layer definitions
Check Test Quality for quality guidelines
Use Fixtures & Factories for test data
See Examples for annotated examples
References#
Testing Overview - FairDM testing conventions
Test Layers - Understanding different test types