Write, review, and maintain tests for the FairDM Django project. Use when asked to create new tests, add test coverage for a module, review existing tests, fix failing tests, or refactor test code. Covers pytest conventions, factory-boy usage, fixture design, test structure (flat mirror of fairdm/ source tree), Django model/form/view/admin/filter testing patterns, registry testing with clean-state fixtures, and performance guard patterns using query-count assertions. Also use when discussing testing strategy, test organization, or when creating new fairdm modules that need accompanying tests.
Never use unittest, TestCase, or setUp/tearDown.
Tests mirror the fairdm/ source tree with test_ prefixes:
fairdm/core/project/models.py → tests/test_core/test_project/test_models.py
fairdm/contrib/contributors/ → tests/test_contrib/test_contributors/
fairdm/registry/config.py → tests/test_registry/test_config.py
fairdm/plugins.py → tests/test_plugins.py
fairdm/db/fields.py → tests/test_db/test_fields.py
See references/structure-map.md for the complete mapping.
Rules:
test_<dirname>/test_<module>.py__init__.pyunit/, integration/, contract/ subdirectories)poetry run pytest # all tests
poetry run pytest tests/test_core/ # one subtree
poetry run pytest -k "test_project" # by name
poetry run pytest -m slow # only slow-marked tests
poetry run pytest --no-header -q # quiet output
Import from the canonical path:
from fairdm.factories import (
ContributorFactory, DatasetFactory, MeasurementFactory,
OrganizationFactory, PersonFactory, ProjectFactory,
SampleFactory, UserFactory,
)
FairDM factories follow an opt-in pattern for creating related metadata (descriptions, dates):
Key Principle: By default, factories create only required fields. No descriptions or dates are auto-created.
Why Opt-In?
unique_together on (related, type). Auto-creating metadata could conflict with test-specific data.Usage Patterns:
# Minimal (no metadata) - DEFAULT behavior
project = ProjectFactory()
assert project.descriptions.count() == 0
assert project.dates.count() == 0
# With metadata (opt-in via kwargs)
project = ProjectFactory(descriptions=2, dates=1)
assert project.descriptions.count() == 2
assert project.dates.count() == 1
# Custom types (validated against model VOCABULARY)
project = ProjectFactory(
descriptions=3,
descriptions__types=["Abstract", "Introduction", "Objectives"]
)
# All core factories support this pattern
dataset = DatasetFactory(descriptions=2, dates=1)
sample = SampleFactory(descriptions=1, dates=2)
measurement = MeasurementFactory(descriptions=2)
Vocabulary Validation:
All description/date types are validated against the model's VOCABULARY.values:
# ✓ Valid - types exist in ProjectDescription.VOCABULARY
project = ProjectFactory(
descriptions=2,
descriptions__types=["Abstract", "Methods"]
)
# ✗ Raises ValueError - "InvalidType" not in VOCABULARY
project = ProjectFactory(
descriptions=1,
descriptions__types=["InvalidType"]
)
Available VOCABULARY types:
ProjectDescription.VOCABULARY.values: Abstract, Introduction, Background, Objectives, ExpectedOutput, Conclusions, Other (+ sample/measurement-specific types)ProjectDate.VOCABULARY.values: Start, EndDatasetDescription.VOCABULARY.values: Similar to ProjectDatasetDate.VOCABULARY.values: Check model for available typesSampleDescription.VOCABULARY.values: Includes SampleCollection, SamplePreparation, etc.MeasurementDescription.VOCABULARY.values: Includes MeasurementConditions, MeasurementSetup, etc.Batch Creation with Metadata:
# All projects get 2 descriptions and 1 date
projects = ProjectFactory.create_batch(5, descriptions=2, dates=1)
# Custom types for all
datasets = DatasetFactory.create_batch(
3,
descriptions=2,
descriptions__types=["Abstract", "Methods"]
)
SubFactory for FK relations — never manually create parent objects when a factory existsfactory.Sequence(lambda n: f"value{n}") for unique fieldscreate_batch(n) for bulk creationPortal-specific factories (e.g. demo app custom samples) are mapped via FAIRDM_FACTORIES
in test settings.
Use directly — no need to redeclare: db, client, rf, admin_user,
django_user_model, django_assert_num_queries.
def test_something(user): # UserFactory()
def test_something(project): # ProjectFactory(owner=user)
def test_something(project_with_datasets): # (project, [3 datasets])
def test_registration(clean_registry): # empty registry, cleaned after test
def test_dynamic_model(unique_app_label): # "test_app_<hex>" for Meta.app_label
# cleanup_test_app_models runs autouse — removes test_app_* from Django app registry
Prefer a factory call over a fixture when the fixture would just wrap a single factory call.
Group tests into classes by subject. One class per logical unit under test:
@pytest.mark.django_db
class TestProjectModel:
def test_creation_with_required_fields(self): ...
def test_uuid_is_unique(self): ...
def test_status_choices(self): ...
@pytest.mark.django_db
class TestProjectCreateForm:
def test_valid_with_required_fields(self): ...
def test_invalid_without_name(self): ...
Naming: test_<what_is_being_tested> — descriptive, no abbreviations.
@pytest.mark.django_db on every class or function that touches the database@pytest.mark.parametrize for repeated logic with different inputs@pytest.mark.slow for tests that take >1 secondNever use wall-clock timing assertions (assert elapsed < 0.1).
Use query-count guards instead:
def test_list_view_query_count(client, django_assert_num_queries):
ProjectFactory.create_batch(10)
with django_assert_num_queries(3):
client.get("/projects/")
Or algorithmic complexity guards (assert O(n) not O(n²)).
See references/exemplar-patterns.md for complete tested patterns covering:
See references/anti-patterns.md for common mistakes and their corrections.
Key configuration (already set up — do not modify without reason):
DisableMigrations classfairdm.setup(apps=["fairdm_demo"]) loads the demo appFAIRDM_FACTORIES maps demo models to their factories__init__.py in any new directoriesfairdm.factories for model instancesTestXxxModel, TestXxxForm, etc.)@pytest.mark.django_dbpoetry run pytest <test_file> to verifyThis skill file supersedes the original specs/002-testing-strategy spec (created January 2026, removed February 2026). The skill format provides more maintainable, agent-friendly documentation of FairDM's testing conventions.