Testing standards#
These rules apply to every change in this repository. CONSTITUTION.md makes them binding. The
sections above Project additions are shared unchanged with our other repositories, so edit
this repository’s rules in that last section only.
1. What gets a test#
A test exists to catch a behaviour that breaks by accident. Before writing one, ask what decides whether the result is right.
A specification, a contract or a computation decides it. Write a test.
Only a person looking at the page decides it. Write no test. Make the change and have it reviewed by eye.
A useful second check is whether the test could fail for any reason other than someone deliberately changing the value it asserts. If it could not, it is a change detector. It only records a decision, and it has to be edited every time that decision changes. Do not write it.
Tested#
The right records, values and fields appear, and the wrong ones do not.
Conditional states: empty, absent, hidden from users without permission.
Link and redirect targets.
Form behaviour: validation, what is saved, where the user is sent afterwards.
Values computed from data, including how a missing value is shown (
—rather than0).Escaping of user and model data.
Ids,
data-attributes and other hooks that a script depends on.Permissions and access control.
Query counts on list and detail pages (section 5).
Not tested#
Wording: headings, labels, captions, help text, button text, empty-state text.
Styling and layout: colours, widths, spacing, stacking, alignment, icons, component variants, the order of sections on a page.
CSS classes in project templates.
Markup that consumers depend on#
A package that publishes template components has consumers who write their own CSS and templates against the markup those components emit. For a published component, that markup is behaviour:
the element it renders,
the classes that make up the component’s styling,
the attributes and slots a caller can pass in.
Decorative choices inside the component stay untested. In a project, all markup is design.
Text#
Text is behaviour only when it is computed from data: pluralised, formatted, or chosen by a condition. Fixed wording is never asserted.
Messages that do a job are tested for the job, not the sentence:
A validation error is asserted by the field it is attached to and its error code (
form.has_error("date", code="future_date")). Raise everyValidationErrorwith acode.A flash message is asserted by its level and that it was added.
An email is asserted by its recipient and the data or link it carries.
Finding elements#
A test that needs an element finds it by data from its fixtures, by role, or by id, never by the surrounding wording. Renaming a heading must not break a behaviour test. When an existing test is anchored on text that changes, update the string and nothing else.
Specifications#
An acceptance criterion states behaviour, never wording or appearance. “Shows the record’s licence” is a criterion. “Headed ‘Licence’” is not, because a criterion is the thing a test is written against.
2. Test-first#
Every behaviour change follows the red, green, refactor cycle.
Red. Write a test and watch it fail for the right reason. A test that passes on its first run is testing nothing new.
Green. Write the least code that makes it pass.
Refactor. Clean up with the tests still green. A refactor that needs an assertion edited has changed behaviour and is not a refactor.
Defects are reproduced first. Write a test that fails with the reported symptom, then fix it. If no test can reproduce it, say so in the pull request.
Changes to wording or appearance skip the cycle. They are not defects. Make the change and move on.
Never weaken an assertion, add skip or xfail, broaden an except, or special-case production
code to make a test pass.
3. Writing tests#
Assert outcomes, not call sequences.
assert response.context["concepts"] == [...]survives a refactor.mock_filter.assert_called_with(...)breaks on one that changed nothing.Real objects over fakes, fakes over mocks. Mock only what is slow, non-deterministic or has side effects outside the test: network calls, email, the clock. Never mock the ORM.
One behaviour per test, named for it:
test_publishing_a_vocabulary_freezes_its_concepts, nevertest_publish_works.Arrange, act, assert, visibly separated, in that order.
Readable over DRY. A test should read as a specification without tracing helpers.
4. Structure and fixtures#
Mirror the source tree. Every test module mirrors the path of the module it exercises:
pkg/models.py→tests/test_models.py,pkg/views/form_views.py→tests/test_views/test_form_views.py. Test subpackages carry__init__.py. One source module that defines several units stays one test module, split by classes.A test whose subject is not a Python module has nothing to mirror:
tests/test_factories.pyteststests/factories.py.tests/test_smoke.pychecks that the package imports and its settings are valid.A suite testing templates or static assets is exempt when the repository declares it:
[tool.forge.conformance] non-mirror-paths = ["tests/test_components/"]
A trailing slash marks a directory prefix. Declaring a path whose subject is a Python module is a review failure.
Group tests into classes, one
Test<Subject>class per unit, so one area can be run on its own:pytest tests/test_models.py::TestConceptModel.One factory per model. Each model has exactly one
factory_boyDjangoModelFactoryintests/factories.py, usingfactory.Sequencefor unique fields andfactory.SubFactoryfor relations. Variants override fields at the call site. They are never factory subclasses.Fixtures wrap factories, and shared setup lives in
conftest.py.def concept(): return ConceptFactory(). A one-off variation calls the factory inline. Test modules hold assertions, not construction.Use the pytest-django toolchain. Database access through
db,transactional_dbor@pytest.mark.django_db. Requests throughclient,admin_clientorrf. Nounittest.TestCase. The tools come pinned in themvp-shared[test]bundle.A run writes files only inside its own directory, and a factory attaches none unless asked.
MEDIA_ROOT, andSTATIC_ROOTwhere anything writes to it, point at a directory the runner creates and removes for the run (tmp_path_factory). That has to hold underpytest-xdist. A factory that can attach a file leaves the field empty by default, and a test that needs a file asks for one (ProjectFactory(with_image=True)). A downstream project inherits a package’s factories without its test settings, so a factory that writes on every build fills that project’s media directory.
5. Django checks#
Check |
How |
|---|---|
No N+1 queries on list and detail pages |
Render with one record and with several, and assert the same query count under |
Migrations are complete |
|
Every model field has |
One parametrised test over every field of every model. Never one test per field, and never asserting the wording. |
Template output for component changes |
Render the template, not only the context. A context key can be right while the template never renders it. |
Translated strings compare correctly |
Compare |
6. Coverage#
Project ≥ 90%, patch ≥ 85%, with a 1% tolerance, as set in
codecov.yml. They are floors, not a ratchet towards 100%.Only Python is measured. Templates are not, so markup left untested under section 1 does not lower the figure. Do not enable template coverage.
A test written only to raise coverage is still bound by section 1.
Project additions#
Version: 1.0.0