Testing Pipelines That Emit Models
What can be tested deterministically in an ML codebase, why model quality is not a unit test, and the layered test strategy that keeps CI fast while still catching the failures that matter.
Conventional CI asserts that given an input, the code produces an expected output. A training pipeline's output is a model whose exact parameters are not predictable and whose quality depends on data that changes. Applying the conventional model directly produces either tests that assert nothing or tests that fail constantly, and the resolution is to separate what is deterministic from what is statistical and test each with the right instrument.
What is deterministic and belongs in fast CI
Data transformations. Given a small fixture input, a preprocessing function produces an exactly known output. This covers most of the code where bugs actually live, and it runs in seconds.
Shape and type contracts. A model given a batch of the declared shape produces output of the declared shape and type, with no NaNs. This catches a large class of integration errors without training anything.
Invariance and equivariance properties. Predictions should be invariant to row order in a batch, to irrelevant feature permutations where applicable, and equivariant to documented transformations. These are properties, not values, so they can be asserted exactly even though the predictions cannot.
Overfitting a tiny batch. Train on ten examples for a few hundred steps and assert the loss approaches zero. This is the single most valuable ML-specific test, because it fails when the model, loss, optimiser or data plumbing are wired incorrectly, and it takes under a minute.
Serialisation round-trips. Save and load a model, assert the outputs match. Catches the version-bundle failures that are otherwise discovered in production.
What is statistical and belongs elsewhere
Model quality cannot be a pass-fail assertion on a single number, because the number has variance from initialisation, data order and hardware. A threshold set tightly enough to catch a real regression will fail on noise, and one set loosely enough to be stable will pass real regressions.
The workable form is a comparison against a recorded baseline with a tolerance derived from measured run-to-run variance, evaluated on a fixed dataset version, run as part of a longer pipeline rather than on every commit. Behavioural tests are more useful still: assert on specific, curated examples that the model must get right, which are stable, interpretable, and fail with a message a human can act on.
When it breaks
Fixtures drift from production data. A test fixture created two years ago no longer resembles what the pipeline receives, so the tests pass while production breaks. Refreshing fixtures from sampled production data, with the sampling automated, is what keeps them honest.
Every test needs a GPU or the full dataset. A suite that cannot run without a cluster is a suite that runs nightly and does not gate anything. Keeping the fast layer runnable on a laptop with tiny fixtures is what makes CI a gate rather than a report.
Flaky tests get disabled rather than fixed. A test that fails one time in twenty because of unseeded randomness or a timing assumption gets skipped, and the coverage disappears silently. Flakiness in an ML test suite is usually a determinism problem with a real fix.
The tests confirm the code and not the intent. A preprocessing test asserting the current output is correct locks in whatever the function does today, including its bugs. Tests written from the specification, not from the observed behaviour, are the ones that catch a regression in meaning rather than in implementation.
12 flashcards for this concept
Click a card to reveal the answer.