Testing

Abacus uses pytest for automated tests, plus local verification scripts for the broader confidence checks that glue linting, smoke paths, and packaging together. The expected workflow is to run targeted tests while you iterate and then run the wider local verification commands before you finish substantial work.

Test Layout

Path What it covers
tests/test_*.py Shared infrastructure such as model IO, paths, package identity, and root-level helpers
tests/mmm/ MMM behaviour at the public surface
tests/mmm/models/ Extracted panel implementation seams
tests/mmm/components/ Adstock and saturation component behaviour
tests/mmm/plotting/ Static plotting helpers and theme/layout behaviour
tests/mmm/optimization/ Budget optimisation logic and wrappers
tests/mmm/diagnostics/ Structured diagnostics compute
tests/mmm/summarization/ Summary/export helpers

When you change a specific module seam, add or update tests in the matching test area instead of only asserting through a broad end-to-end test.

Core Commands

Fast targeted runs

pytest tests/<path>/test_*.py -v
pytest tests/mmm/plotting/test_theme.py --no-cov -q
pytest tests/mmm/models/test_panel_serialize.py --no-cov -q

Use targeted runs first. They are faster to interpret and make regressions easier to localise.

Whole-suite pytest

make test

This installs the test extras and runs pytest with the local runtime defaults from the Makefile.

Local verification

make verify_local
make verify_local_all

The Makefile defines the command graph:

Target Checks run
verify_local check_format, then check_lint, then test
check_format Install lint extras; Ruff format check on abacus tests scripts runme.py
check_lint Install lint extras; Ruff check on the same paths; configured MyPy
test Install test extras; the full configured pytest suite with local runtime defaults
smoke_mmm Run the full runme.py --demo timeseries workflow with unchanged demo sampling budgets
verify_package Install the build tool; build and clean-install the wheel under both reviewed dependency profiles; check requirements, imports, fitting, prediction and persistence
verify_local_all verify_local, then verify_package

verify_local does not include an explicit byte-compilation step or the separate smoke_mmm target. Run make smoke_mmm when you intend to verify execution of the full demo. For a small execution check, use the bounded software smoke, which explicitly skips holdout validation. MyPy checks the files selected in pyproject.toml, not the whole package. Lint targets do not check ignored scratch scripts in sandbox/.

Inspect the current command graph without executing it:

make -n verify_local verify_local_all smoke_mmm

Pre-commit static checks

With the lint extras installed in the active environment, run:

python3 -m pre_commit validate-config
python3 -m pre_commit run --all-files

The local hook configuration runs Ruff format checking, Ruff lint and the configured MyPy scope. It uses the active python3 environment without downloading hook environments. Each hook checks its full configured scope on every invocation, even when only documentation changed. The hooks do not rewrite files.

Run pytest separately for test coverage. These static hooks do not replace make verify_local or packaging checks. Manual invocation does not install a Git hook; use python3 -m pre_commit install only if you want automatic checks on local commits.

Packaging smoke

make verify_package

Run this when any of the following changed:

  • packaging metadata in pyproject.toml
  • import surfaces or compatibility facades
  • bundled assets under abacus/
  • install-time behaviour or README/package artefacts

Runtime Environment

Some test paths need writable cache directories. The recommended defaults are:

export PYTENSOR_FLAGS="base_compiledir=/tmp/pytensor,linker=py"
export JAX_PLATFORMS=cpu
export XDG_CACHE_HOME=/tmp

The Makefile applies these defaults to test and smoke_mmm. Direct pytest commands use your current environment; export them explicitly when needed. The package verifier sets its own temporary PyTensor cache and Python linker.

Special Cases

Plotting tests

Prefer tests that inspect stable properties such as axes, labels, colours, sizes, rcParams, and return types. Avoid brittle pixel-perfect assertions.

For a focused plot run, you can disable Numba JIT explicitly:

NUMBA_DISABLE_JIT=1 pytest tests/mmm/test_plot.py --no-cov -q

Save/load and compatibility work

If you change model serialisation, identity strings, or import compatibility, add tests that prove older saved data or old import paths still work where that compatibility is expected.

Packaging and bundled assets

If you add or move package data, use make verify_package so the change is checked against an installed wheel rather than only the editable repo checkout. See dependency verification profiles for their interpreter/platform scope and lower-bound limits. Passing tests and installation smoke checks does not establish convergence, statistical validity or causal identification.

What to Run Before You Finish

Small, localised change

  • Targeted pytest
  • Targeted ruff check

Moderate code change

  • Targeted pytest
  • make check_lint
  • make smoke_mmm

Broad or risky change

  • make verify_local
  • make verify_package if packaging or bundled assets changed

Writing Good Tests

  • Test observable behaviour, not implementation noise.
  • Keep fixtures close to the layer you are testing.
  • Prefer additive compatibility tests when preserving old behaviour.
  • Use small synthetic data where possible.
  • For plotting and serialisation, assert the stable contract rather than fragile internals.