Skip to content

Testing

Getting started

Run all tests from the root directory:

pytest

Useful Pytest flags

Some useful flags to improve the DX of testing.

Only re-run failing tests

pytest -lf

Run a specific test file, e.g. run all tests in only test_npda_user_model_actions.py

pytest project/npda/tests/permissions_tests/test_npda_user_model_actions.py

Use :: notation to run a specific test within a file e.g.:

pytest project/npda/tests/permissions_tests/test_npda_user_model_actions.py::test_npda_user_list_view_rcpch_audit_team_can_view_all_users

Start a completely clean run, clearing the cache (usually not required)

pytest --cache-clear

Run tests through keyword expression

NOTE: this is sometimes slightly slower.

pytest -k "MyClass and not method"

pytest.ini config file

Explanations and notes on our global pytest.ini configuration.

Creating new tests

Test files are found as any .py prepended with test_:

# SET FILENAME FORMATS OF TESTS
python_files = test_*.py

Flags set for all pytest runs

# RE USE TEST DB AS DEFAULT
addopts =
    --reuse-db
    -k "not examples"
  • --reuse-db allows a specified starting state testing database to be used between tests. All tests begin with this seeded starting state. The testing db is rolled back to the starting state after each state.

Test Database

Every first test in a file should include the following fixtures to ensure the test database is correctly set up for when a particular test file is run independently:

@pytest.mark.django_db
def test_npda_user_list_view_users_can_only_see_users_from_their_pdu(
    seed_users_fixture,
    seed_groups_fixture,
):

seed_users_fixture

The testing database should include 8 NPDAUsers.

At each of the 2 PDUs, GOSH and Alder Hey:

seed_users.py
GOSH_PZ_CODE = "PZ196"
ALDER_HEY_PZ_CODE = "PZ074"

The following 4 user types are seeded:

seed_users.py
users = [
    test_user_audit_centre_reader_data,
    test_user_audit_centre_editor_data,
    test_user_audit_centre_coordinator_data,
    test_user_rcpch_audit_team_data,
]

seed_groups_fixture

Uses the groups_seeder to set Groups for NPDAUsers.

Factories

Factories enable the intuitive and fast creation of testing instances, particularly in cases where multiple related models are required.

Factories are set up to set sensible defaults wherever possible, which can be overridden through keyword arguments.

Ideally, in all tests, if a model instance is being created, it should be done through its model Factory.

NPDAUserFactory

Example usage below.

NOTE: we do not need to manually create OrganisationEmployers and PaediatricsDiabetesUnit with associations.

Once an instance of NPDAUserFactory is created, the related models will also be created and assigned the relations. These are set using the organisation_employers kwarg, with the value being an array of pz_codes as strings.

seed_users.py
# GOSH User
new_user_gosh = NPDAUserFactory(
    first_name=first_name,
    role=user.role,
    # Assign flags based on user role
    is_active=is_active,
    is_staff=is_staff,
    is_rcpch_audit_team_member=is_rcpch_audit_team_member,
    is_rcpch_staff=is_rcpch_staff,
    groups=[user.group_name],
    view_preference=(
        VIEW_PREFERENCES[2][0]
        if user.role == RCPCH_AUDIT_TEAM
        else VIEW_PREFERENCES[0][0]
    ),
    organisation_employers=[GOSH_PZ_CODE],
)

# Alder hey user
new_user_alder_hey = NPDAUserFactory(
    first_name=first_name,
    role=user.role,
    # Assign flags based on user role
    is_active=is_active,
    is_staff=is_staff,
    is_rcpch_audit_team_member=is_rcpch_audit_team_member,
    is_rcpch_staff=is_rcpch_staff,
    groups=[user.group_name],
    organisation_employers=[ALDER_HEY_PZ_CODE],
)

PatientFactory

Once an instance of PatientFactory is created, a related TransferFactory instance is also generated with the associated PaediatricsDiabetesUnitFactory instance.

PaediatricsDiabetesUnitFactory

Multiple parent factories require the instantiation of multiple PaediatricsDiabetesUnitFactorys.

As there is a composite unique constraint set on the [pz_code, ods_code] attributes, we do not want to create multiple instances with duplicate values; instead, we want to mimic the Django ORM's .get_or_create() method.

This is emulated using an override on this factory's ._create() method:

paediatrics_diabetes_unit_factory.py
@classmethod
def _create(cls, model_class, *args, **kwargs):
    """
    Custom create method to handle get_or_create logic for PaediatricsDiabetesUnit.

    Each PDU has a composite unique constraint for pz_code and ods_code. This mimics
    a get or create operation every time a new PDUFactory instance is created.
    """
    pz_code = kwargs.pop("pz_code", None)
    ods_code = kwargs.pop("ods_code", None)

    if pz_code and ods_code:
        pdu, created = PaediatricDiabetesUnit.objects.get_or_create(
            pz_code=pz_code,
            ods_code=ods_code,
        )
        return pdu

    return super()._create(model_class, *args, **kwargs)

Performance testing

Use this command to run performance tests (off by default):

s/test -m performance -s