Skip to content

Factories and Fixtures

Pytest and FactoryBoy factories and fixtures enable DRY code, allowing similar object instances to be defined once, and shared many times across the test directory, enabling faster and more consistent test-driven development.

For example, many of the tests rely on the creation of a fully-completed audit, which is comprised of the following:

  1. Case model with linked Organisation(s), through the Site conduit model

  2. Linked Registration model

    1. Linked AuditProgress model
    2. Linked KPI model
    3. Linked FirstPaediatricAssessment model
    4. Linked EpilepsyContext model
    5. Linked MultiaxialDiagnosis model
    6. Linked Assessment model
    7. Linked Investigations model
    8. Linked Management model

Following the best-practice principle of DRY code, we use FactoryBoy factories to define the creation of Epilepsy12 Case instances, with default values set once, minimising repeated code. Whenever an object is created using the 'top-level' e12_case_factory, all the linked dependency models have generated automatically, with default values that can be overridden.

conftest.py

The conftest.py file registers fixtures globally to be used in any test without import. Available factories are imported and registered like so:

from epilepsy12.tests.factories import (
    E12CaseFactory,
    ... # other factories
)

register(E12CaseFactory)  # => e12_case_factory

When factories are registered this way, their default fixture name becomes the lowercase, under-score version of the class name. In this case, E12CaseFactory's fixture becomes e12_case_factory, which can be used like so:

@pytest.mark.django_db
def test_example(e12_case_factory):

    fully_completed_case = e12_case_factory()

Conservative Database Access

By default, Pytest does not allow database access in its tests.

If you require a test to access the database, mark it with the @pytest.mark.django_db decorator.

Alternatively, you can feed it in as a fixture. However, we only use marks for consistency.

Using E12Factories

As factories have been registered within conftest.py, whenever a factory is required in a test, pass in the lowercase, under-scored fixture name into the test arguments:

@pytest.mark.django_db
def test_example(
    e12_case_factory
):
    ...

Each E12 model has an associate factory, whose fixtures are namespaced using the e12_LOWERCASE_UNDERSCORE_MODELNAME_factory e.g. pattern:

register(E12AntiEpilepsyMedicineFactory)  # => e12_anti_epilepsy_medicine_factory
register(E12AssessmentFactory)  # => e12_assessment_factory
register(E12CaseFactory)  # => e12_case_factory
register(E12ComorbidityFactory)  # => e12_comborbidity_factory
register(E12EpilepsyContextFactory)  # => e12_epilepsy_context
register(E12EpisodeFactory)  # => e12_episode_factory
register(E12FirstPaediatricAssessmentFactory)  # => e12_first_paediatric_assessment_factory
register(E12ManagementFactory)  # => e12_management_factory
register(E12MultiaxialDiagnosisFactory)  # => e12_multiaxial_diagnosis_factory
register(E12RegistrationFactory)  # => e12_registration_factory
register(E12SiteFactory)  # => e12_site_factory
register(E12SyndromeFactory) # => e12_syndrome_factory
register(E12UserFactory)  # => e12_user_factory

Unique emails in E12UserFactory

E12UserFactory generates emails with a factory.Sequence, which restarts at 0 in every pytest process. That is normally fine because each test runs inside a transaction that is rolled back. However, two things combine to break that assumption:

  1. The test suite runs with --reuse-db (see pytest.ini), so the test database and any committed data persist between runs.
  2. Session-scoped fixtures such as seed_users_fixture use django_db_blocker.unblock() and commit the users they create (~20 users with sequenced emails e12_test_user_0@nhs.net and up) into that reused database.

In a later pytest process the sequence restarts, so the first factory-created user collides with the committed e12_test_user_0@nhs.net and fails with:

django.db.utils.IntegrityError: duplicate key value violates unique constraint "epilepsy12_epilepsy12user_email_key"

To make this collision impossible, the generated email includes a per-process UUID fragment alongside the sequence:

_run_id = uuid.uuid4().hex[:8]
email = factory.Sequence(lambda n: f"e12_test_user_{n}_{E12UserFactory._run_id}@nhs.net")

The sequence keeps users distinct from each other within a run; the UUID fragment keeps the run's emails distinct from any users committed by previous runs. Tests should never depend on the exact generated email format — look users up by a stable attribute (e.g. first_name for the seeded users) instead.

Don't assert on generated emails

If a test asserts a user's email address, pass email="..." explicitly to the factory rather than relying on the generated value.

Usage

For most test cases, which require multiple different linked models (e.g. a Registration attached to a MultiaxialDiagnosis), you should instantiate starting from the e12_case_factory:

@pytest.mark.django_db
def test_example(
    e12_case_factory
):
    case = e12_case_factory()

This will create and save a fully-registered and audit-complete Case, accessible using the case variable.

Accessing values and overriding defaults

Default attributes of the Case model can be overridden:

@pytest.mark.django_db
def test_example(
    e12_case_factory
):
    case = e12_case_factory(
        first_name = "Bob",
        surname = "Dylan",
    )

Dependency factory attributes can be directly overridden, up to 2 dependencies down (the factories directly related to a Registration), using the dunder-format:

@pytest.mark.django_db
def test_example(
    e12_case_factory
):
    case = e12_case_factory(

        # Case.first_name
        first_name = "Bob",

        # Case.surname
        surname = "Dylan",

        # Registration.registration_date
        registration__registration_date=date(2023,1,1),

        # Assessment.childrens_epilepsy_surgical_service_referral_criteria_met WHERE Assessment.registration is linked to this instance
        registration__assessment__childrens_epilepsy_surgical_service_referral_criteria_met=True,

        # Investigations.mri_brain_requested_date WHERE Investigations.registration is linked to this instance
        registration__investigations__mri_brain_requested_date=date(2023,4,1)
    )

Values can be directly accessed up to 2 dependencies down:

print(case.first_name) # => Bob
print(case.registration) # => Epilepsy12 registration for Bob Dylan on 2023-01-01
print(case.registration.multiaxialdiagnosis) # => Multaxial diagnosis for Bob Dylan
print(case.registration.multiaxialdiagnosis.syndrome_present) # => True

Accessing Multiaxial_Diagnosis attribute

NOTE: to access Multiaxial_Diagnosis in this way, use the lowercased, non-underscored name: multiaxialdiagnosis

These all return Django model objects, which can be used in the classic Django way if further customisation is required for tests:

epilepsy12/tests/common_view_functions_tests/calculate_kpi_tests/test_measure_5.py
from epilepsy12.models import (
    Registration,
    Syndrome,
)

@pytest.mark.django_db
def test_measure_4_mri_syndromes_ineligible(
    e12_case_factory,
    ...
):

    ...

    case = e12_case_factory()

    # get registration for the saved case model
    registration = Registration.objects.get(case=case)

    # get syndrome for registration
    current_syndromes = Syndrome.objects.get(
        multiaxial_diagnosis=registration.multiaxialdiagnosis
    )

    ...

Flags

Specific factories contain flags that override multiple values if set True. For example, the E12_Assessment_Factory assigns consultant_paediatrician_referral_made=True by default, with consultant_paediatrician_referral_date and consultant_paediatrician_input_date.

The Assessment.no_referral_consultant_paediatrician=True flag can be used to override this:

@pytest.mark.django_db
def test_testing(
    e12_case_factory
):

    case = e12_case_factory(
        registration__assessment__no_referral_consultant_paediatrician=True
    )

    print(case.registration.assessment.consultant_paediatrician_referral_made) # => False
    print(case.registration.assessment.consultant_paediatrician_referral_date) # => None
    print(case.registration.assessment.consultant_paediatrician_input_date) # => None