Skip to content

4. Testing

A test is useful when it can fail for a reason the contributor understands. Its setup, assertion, and execution boundary should make that reason recoverable. LychD chooses ordinary typed fixtures and explicit evidence classes over a second language for describing behavior.

Decision Outcome

pytest is the primary engine. Its fixtures and parametrization fit the repository more directly than verbose unittest scaffolding or keyword-heavy BDD. Polyfactory builds valid Pydantic models and plain types where fabrication helps; an explicit local factory remains appropriate when the fixture's meaning is part of the assertion.

1. The Engine (Pytest + Xdist)

All Core test, lint, format-check, type and coverage processes run in the explicit OCI image built from Containerfile.tests, selected through the Makefile and scripts/container_checks.sh. The host harness may build and launch that image; it does not execute a host Vessel or fall back to host pytest when the engine is unavailable. Application construction and ASGI/domain helpers inside the image are test techniques, not another deployment profile. Installed-runtime qualification instead needs the actual rebuilt production Vessel image and declared boundary.

Tests run serially by default because some probes share host-resource boundaries. A caller may select bounded pytest-xdist parallelism. Each ordinary invocation receives unique scratch under the disposable container's .cache/pytest, captured logs, and pytest's native compact report without an output wrapper. Caller-owned scratch and verbose diagnostics remain explicit choices. The exact switches belong in CONTRIBUTING.

Start with the smallest relevant source boundary. The directory layout makes that selection visible:

Directory Evidence sought
tests/unit Isolated domain, utility, and adapter contracts.
tests/integration The named cross-layer, filesystem, database, or substitute conjunction.
tests/architecture Package boundaries, release declarations, public contracts, and documentation topology.
tests/web HTTP and browser-projection contracts at their declared server boundary.

2. The Taxonomy (Markers)

Markers use strict pytest.mark semantics: unit names isolated contracts, integration names joined layers, container selects explicit disposable-container receipts, and slow marks expensive work such as heavy model loading. Strictness catches misspelled markers; it does not populate them. Most present selection follows directories: unit and slow are unapplied, and only a few integration tests carry integration. Consequently M=unit collects nothing today; select the directory instead.

By default, make test and make check exclude container. An explicit marker expression replaces that exclusion. make test-containers admits that profile with its separate dependencies and an explicitly admitted Docker daemon socket through the Docker-only PostgreSQL harness. Ordinary checks receive no daemon socket or network; this separate PostgreSQL harness has its own declared socket/network authority. Host and external systems stay opt-in and need named receipts. Network and provider dependence enter a deterministic test only when its declared integration boundary requires them.

3. The Fabricator (Polyfactory)

Generated fixture data must remain valid under the actual schema. Factories help construct it; they do not explain which property matters or conceal a validator prerequisite. A failing fabrication and a failing system behavior are distinct diagnoses.

4. The Quality Gate (Coverage)

pytest-cov measures branch coverage against the pyproject.toml 80% floor and deliberate structural exclusions. make coverage enforces it serially with the ordinary not container selection. Default tests, the Python umbrella, pull-request checks, and release-candidate CI do not pass --cov. Coverage is an opt-in gate, not an implicitly passed release condition.

Pull-request, dev and main Core Python CI runs the explicit make test-ci selection, using in-process substitutes inside that OCI image, without a model or daemon socket in the test process. The outer build/launch still requires a working OCI engine. Full ordinary tests and disposable PostgreSQL receipts remain separate jobs in the manually dispatched Repository Checks workflow. A core-CI pass does not establish those broader results or waive affected-boundary verification. The normal make test and make check selections remain unchanged.

5. Runtime Surface Probes

A repository test establishes its recorded conjunction. Real systemd, Podman, PostgreSQL, GPU, model, and browser behavior requires the corresponding maintained operator receipt; mocks do not inherit the authority of the systems they replace. State of Work records that distinction. Host-native Core deployments and their test fixtures are retired; frozen native-profile receipts retain only their dated historical scope and cannot qualify the current test image or a rebuilt production image.

Altar and GenUI probes may inspect rendered semantic contracts and preserve DOM or screenshot evidence. A missing prerequisite is BLOCKED, not a fabricated behavioral FAIL. Server state remains authoritative, and a rendered success message must not substitute for the effect record.

Consequences

Retain failures with their reproducing tests. Choose directories, marks, and opt-in boundaries for the claim being made, then widen when a change crosses those boundaries. Any exception needs its own stated scope and receipt. The result is a suite a contributor can reason about, and a clear account of what still needs to be witnessed on real iron.