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:
-
Casemodel with linkedOrganisation(s), through theSiteconduit model -
Linked
Registrationmodel- Linked
AuditProgressmodel - Linked
KPImodel - Linked
FirstPaediatricAssessmentmodel - Linked
EpilepsyContextmodel - Linked
MultiaxialDiagnosismodel - Linked
Assessmentmodel - Linked
Investigationsmodel - Linked
Managementmodel
- Linked
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:
- The test suite runs with
--reuse-db(seepytest.ini), so the test database and any committed data persist between runs. - Session-scoped fixtures such as
seed_users_fixtureusedjango_db_blocker.unblock()and commit the users they create (~20 users with sequenced emailse12_test_user_0@nhs.netand 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:
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