Testing ML systems: unit, data, behavioural, slice and regression tests
Before this lesson: lesson 01 (data contracts, reproducible runs); evaluation metrics and data leakage; logistic regression; the binomial distribution (common distributions) and hypothesis testing. Time: about 60 minutes, plus the notebook.
Why this matters #
A model can pass every code test and still be wrong. A feature computed with a future timestamp, a model that uses the reviewer's name instead of the review text, or a new version that is better on average but worse in the mountains: none of these raises an error. The tests of this lesson find them. They become the release gate in CI (lesson 03) and in the model registry (lesson 06). LLM eval suites (lesson 09) use the same ideas. "How would you test this model?" is also a standard interview question.
1. Predict first #
2. The picture #
Two kinds of correctness. Code correctness is exact. A function gets an input and must return
a known output, so the test is assert f(x) == y. Learned behaviour is statistical. You cannot
write down the right output for every input. So you test properties (relations between inputs
and outputs) with tolerances, and rates (the share of cases that fail) over many cases.
The simplest model. Take a logistic model of the probability of rain in the next hour,
where is the relative humidity, is the temperature, is the sigmoid, and are learned. A directional test says: "more humidity must not lower the probability of rain". The sigmoid is increasing, so grows with exactly when . For this model the property holds for every input if one number has the right sign. One check proves a global property.
The general case. For a neural network , the same test computes at the inputs you chose. Here is the unit vector of the humidity feature and is a step. The test says nothing about inputs you did not choose. This is the central limit of behavioural testing: a test samples the input space. It can find a violation (a counterexample). It cannot prove that the property holds everywhere.
Draw a few suites of 20 random cases, then show where the test fails. The map is the model's probability of rain (darker blue = higher). Each case is a short arrow from to ; it fails (magenta) when the probability drops by more than 0.01. The "network" has a small dip near , °C that covers about 2.4% of the inputs. A suite of 20 cases finds it only about 38% of the time. Switch to "logistic": there the sign of settles the question for all inputs.
This picture decides where each test runs. Cheap, exact tests (code and data) run on every commit. Slower, statistical tests (behaviour and slices) run before a model is promoted to production.
A logistic rain model has and . How many test cases do you need to show that more humidity never lowers its probability of rain?
and is increasing, so everywhere when . The sign of does not matter for this property. Sampling is needed only when you cannot read the property off the model, as for a neural network.
3. Definitions and main results #
Test types #
The examples are for the running example of this track: a rain nowcast that gives the probability of rain in the next hour at a weather station.
| Type | What it checks | Example | When it runs |
|---|---|---|---|
| code unit | one function, exact output | the Kelvin-to-Celsius feature function; the loss is minimal for a perfect forecast; output shapes | every commit, seconds |
| data | the data contract and leakage | the contract of lesson 01; no station in two splits; no feature timestamp after the label time; no duplicates between train and test | every commit on a sample; before every training run on the full data |
| model, before training | the training code can learn | the loss falls on a tiny batch; the model can overfit one batch; outputs are probabilities in | every commit, about a minute |
| model, behavioural | learned capabilities | MFT, INV and DIR tests (below) | before promotion |
| slice and regression | candidate against production, overall and per slice | accuracy per region, season and station type; no slice drops by more than | before promotion (the release gate) |
| infrastructure | the system around the model | the model loads in the serving image; online and offline predictions agree on logged requests; latency and memory budgets; rollback works | CI after the image build; canary |
A data test is a test of the data, not of code: it runs the checks of a data contract and looks for leakage. A pre-training test (for example "the model can overfit one batch of 8 examples") checks that the training code can learn at all. If a small network cannot reach 100% training accuracy on 8 points, the code has a bug: wrong labels, a learning rate of zero, or a gradient that does not reach the weights.
Behavioural tests (CheckList) #
Let be the model's score, a set of perturbed copies of an input , and a tolerance. CheckList (Ribeiro et al., 2020) defines three test types.
- Minimum functionality test (MFT): simple inputs with known labels , aimed at one capability. A case fails if the thresholded score differs from . It is a unit test for a capability. Example: "The food was not good." must be negative.
- Invariance test (INV): is label-preserving (the right label does not change). A case fails if . Example: replace the reviewer's name.
- Directional expectation test (DIR): has a known effect direction . A case fails if for some . Example: appending "The staff was rude." must not raise the positive score ().
The failure rate of a test is (failing cases) / (tested cases). INV and DIR need no labels: they compare predictions with predictions. So they also run on unlabeled production inputs. MFT comes from software unit tests. INV and DIR come from metamorphic testing: you do not know the right output, but you know how the output must relate to the output of a changed input.
How to design them, in five steps.
- List the capabilities the model must have. For sentiment: vocabulary, negation, named entities. For a weather model: the response to humidity, to pressure, to the time of day.
- Write each one as a relation between inputs and outputs: a fixed label (MFT), equality (INV) or an order (DIR).
- Write a template or a perturbation function that generates many cases.
- Check with domain knowledge that the perturbation really preserves the label or the direction. Swapping city names preserves the label of a restaurant review. For a weather model it does not: the location carries information.
- Choose , a maximum failure rate, and whether a failure blocks a merge or only reports.
What they can show: a concrete counterexample; a shortcut (a feature that predicts the label in the training data for a wrong reason: the score moves when only a name changes); a missing capability (negation). They treat the model as a black box, so they also compare models trained on different data. What they cannot show: that the property holds for all inputs; accuracy or calibration; failures that nobody thought of. And a test is only as valid as its perturbation.
A sentiment model uses a bag of words: it counts each word and ignores the order. It has 95% test accuracy. What does it give for "the food was good, not bad" and "the food was bad, not good"?
Both sentences contain the same words with the same counts, so the model receives identical inputs and must return identical outputs. No weights and no amount of data can fix this. An MFT on negation exposes it; the test accuracy does not, because such pairs are rare in the test set.
A rain model gets an input with the station's pressure lowered by 5 hPa (falling pressure usually brings rain). What kind of test is "the probability of rain must not go down"?
The perturbation has a known effect direction, so it is a DIR test: a case fails if . It needs no label. An INV test would demand that the score does not move at all, which is wrong here: lower pressure should raise the probability of rain.
Result: what a clean run shows (the rule of three) #
Theorem (rule of three). If independent test cases all pass, the one-sided 95% upper confidence bound on the failure rate is
Why this is true, first in words. If the model failed often, a clean run of cases would be very unlikely. The bound is the failure rate at which a clean run becomes too unlikely (less than 5%) to believe.
Proof.
- If the true failure rate is and the cases are independent, then .
- The 95% upper bound is the largest for which this outcome still has probability at least 0.05: .
- Solve: , so .
- Take logs: . For small , , so .
So 300 passing cases show (exactly ), and 3,000 show . The bound is about the distribution that generated the test cases. Cases from one template are not a sample of production inputs.
Move and the true failure rate , then run 1,000 suites. The violet curve is the exact bound ; the dashed line is . The two agree for . When the magenta line of lies above the bound, fewer than 5% of suites pass (blue squares). With and , about 4.9% of suites still pass: this is the 95% level at work.
The exact (Clopper–Pearson) bound
The rule of three is the special case of the exact one-sided Clopper–Pearson bound for a
binomial proportion: with failures in cases, the 95% upper bound is the 0.95 quantile of
. For , the Beta$(1, n)$ distribution has CDF , so its
0.95 quantile solves : the same . In SciPy: stats.beta.ppf(0.95, 1, n).
An invariance suite of 1,000 independent cases has no failure. What can you claim, with 95% confidence?
The rule of three gives , about . The claim holds only for the distribution that generated the cases. A clean suite never proves invariance, and it says nothing about production inputs that the template does not produce.
Slice and regression tests #
A slice is a subset of rows defined by metadata: region, season, station type, sensor model, a rare class. A regression test scores the candidate model and the production model on the same frozen test set, overall and per slice. It flags a slice when the slice has at least rows and its metric drops by more than . The ML Test Score paper (test Model 6) suggests two kinds of threshold: absolute ("error on slice below 5%") and incremental ("error on slice up by less than 1% against the previous model").
Small slices are noisy. An accuracy measured on rows has standard error
which is for and . A drop of 3 points on 100 rows is less than one standard error.
Pair the comparison. Both models are scored on the same rows, so most rows give the same result for both: easy rows are right for both, hard rows are wrong for both. These rows carry no information about the difference. McNemar's test uses only the rows where the models disagree, the discordant pairs: = old right and new wrong, = old wrong and new right. The drop in accuracy is . If the two models are equally accurate, each discordant row goes either way with probability , so . The exact two-sided p-value is with .
Start with the worked example, then try the other presets. Blue bars are the old model, orange bars the new one; the aqua dashed line is the old alpine accuracy minus . Overall the new model wins, while the alpine slice loses 10 points. Move the slider "disagree (b + c)/n": the drop stays the same, but McNemar's p-value changes. Fewer disagreements mean a clearer verdict.
Why pairing gives a smaller standard error
Let if the old model is right on row , and if the new one is. The drop is the mean of . Then . An unpaired test assumes . But the two models are right on the same easy rows, so the covariance is large and positive, and the paired variance is much smaller. For the alpine slice of section 4, the paired standard error of the drop is , while the unpaired one is .
The ML Test Score #
Breck et al. (2017) list 28 tests in four sections of seven:
| # | Features and data | Model development | ML infrastructure | Monitoring |
|---|---|---|---|---|
| 1 | feature expectations are captured in a schema | model specs are reviewed and submitted | training is reproducible | dependency changes result in notification |
| 2 | all features are beneficial | offline and online metrics correlate | model specs are unit tested | data invariants hold for inputs |
| 3 | no feature's cost is too much | all hyperparameters have been tuned | the ML pipeline is integration tested | training and serving are not skewed |
| 4 | features adhere to meta-level requirements | the impact of model staleness is known | model quality is validated before serving | models are not too stale |
| 5 | the data pipeline has appropriate privacy controls | a simpler model is not better | the model is debuggable | models are numerically stable |
| 6 | new features can be added quickly | model quality is sufficient on important data slices | models are canaried before serving | computing performance has not regressed |
| 7 | all input feature code is tested | the model is tested for considerations of inclusion | serving models can be rolled back | prediction quality has not regressed |
Scoring. Each test gets 0.5 points if it is run manually with documented and shared results, and 1 point if a system runs it automatically and repeatedly. Sum the points per section. The final score is the minimum of the four section sums. The authors chose the minimum because all four areas matter: a team must cover each one to raise the score. Reading the final score (Table V of the paper):
| Score | Meaning |
|---|---|
| 0 | more of a research project than a production system |
| possible serious holes in reliability | |
| a first pass at basic productionisation; more investment may be needed | |
| reasonably tested; more tests may be automated | |
| strong automated testing and monitoring, fit for mission-critical systems | |
| above 5 | exceptional automated testing and monitoring |
Using it to find missing tests. Score each section. The lowest section is the bottleneck: work there first. Inside it, choose the tests whose failure would cost the most, and turn manual tests into automated ones (+0.5 each).
Click tests to change their state and watch the final score. Each test cycles through not done, manual (0.5) and automated (1). The section with the lowest sum has a magenta frame: it alone sets the score. Raise any other section and nothing changes. Compare the final score with the mean.
A system has the section sums data 3.5, model 2.0, infrastructure 4.0, monitoring 1.5. What is its ML Test Score, and where should the team invest first?
The final score is the minimum of the four sections, , which is in : a first pass at basic productionisation. Only a better monitoring section raises the score; for example, automate the data-invariant and skew checks on serving inputs.
Answers to section 1. (1) Yes. Section 4 shows a model that gains 1.7 points overall and loses 10 points on alpine stations. The ML Test Score paper gives a stronger example: global accuracy up by 1%, accuracy for one country down by 50%. (2) About 1%. The rule of three gives . A failure rate below 1% is still possible after 300 clean cases.
4. Worked example #
(a) A slice regression. A rain classifier is tested on 900 lowland and 100 alpine cases.
| slice | rows | old accuracy | new accuracy |
|---|---|---|---|
| lowland | 900 | 0.90 (810 correct) | 0.93 (837 correct) |
| alpine | 100 | 0.80 (80 correct) | 0.70 (70 correct) |
| all | 1,000 |
Overall: +1.7 points. On alpine stations: −10 points. With and , the slice test blocks the release.
Is the alpine drop real or noise? Build the paired table on the 100 alpine cases:
| new right | new wrong | |
|---|---|---|
| old right | 66 | |
| old wrong | 16 |
Check the margins: old ✓, new ✓, total ✓.
- Discordant rows: . The drop is ✓.
- Under "equally accurate", .
- .
- Two-sided: . The drop is unlikely to be noise.
An unpaired comparison of 0.80 and 0.70 gives and only , because it ignores that 82 of the 100 rows agree.
(b) An ML Test Score. A nowcasting system has:
- data: schema checks, feature-code unit tests and privacy controls automated (); a feature-importance study and a feature-cost review done by hand () → 4.0;
- model: spec review and slice metrics automated (); tuning and a simple-baseline comparison done by hand () → 3.0;
- infrastructure: model specs unit tested (1); a manual canary and a documented rollback () → 2.0;
- monitoring: model staleness checked by hand each week (0.5) → 0.5.
Final score: , "possible serious holes in reliability". Monitoring is the bottleneck. Automate Monitor 2 (run the lesson 01 contract on serving inputs) and Monitor 3 (recompute features offline for logged requests and compare). Monitoring becomes , and the final score becomes . Now infrastructure is the bottleneck: automate the canary (Infra 6) and the rollback (Infra 7).
(c) INV and DIR tests for a ground-motion model. In seismology, a ground-motion model predicts how strongly the ground shakes at a site during an earthquake. Here it predicts the peak ground acceleration (PGA) from the magnitude , the distance to the source, and , the average shear-wave velocity of the top 30 m of soil (low = soft soil).
- DIR: at fixed and must not increase PGA (shaking weakens with distance).
- DIR: at fixed and must not decrease PGA.
- INV: the record id, the station code (if it is not a model input) and the order of the records must not change any prediction.
- Not a valid DIR test: "softer soil (lower ) must raise PGA". It holds for weak shaking, but soft soil responds nonlinearly and can reduce PGA in strong shaking. This test would report correct physics as a failure.
5. In code #
The notebook builds the test harness. Here NumPy, SciPy and scikit-learn check the claims of this lesson.
import numpy as np
from scipy import stats
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.neural_network import MLPClassifier
# 1. A bag of words cannot see word order
texts = ["the food was good, not bad", "the food was bad, not good"]
X = CountVectorizer().fit_transform(texts).toarray()
print((X[0] == X[1]).all()) # True: equal inputs, so equal scores for ANY weights
# 2. Rule of three: 0 failures in n cases -> 95% upper bound on the failure rate
n = 300
print(round(1 - 0.05 ** (1 / n), 5), round(stats.beta.ppf(0.95, 1, n), 5), 3 / n)
# 0.00994 0.00994 0.01
# 3. The slice example of section 4: overall gain, slice loss, paired test
old_overall = (900 * 0.90 + 100 * 0.80) / 1000
new_overall = (900 * 0.93 + 100 * 0.70) / 1000
print(round(old_overall, 3), round(new_overall, 3)) # 0.89 0.907
print(round(stats.binomtest(4, 18, 0.5).pvalue, 3)) # 0.031 (McNemar, exact)
# 4. A pre-training test: a small network must fit one tiny batch
rng = np.random.default_rng(0)
X8, y8 = rng.normal(size=(8, 5)), np.array([0, 1, 0, 1, 1, 0, 1, 0])
mlp = MLPClassifier(hidden_layer_sizes=(32,), solver="lbfgs", max_iter=500, random_state=0)
print(mlp.fit(X8, y8).score(X8, y8)) # 1.0
stats.beta.ppf(0.95, 1, n) is the exact one-sided bound for 0 failures; it equals
. The overfit test is fast, seeded and has a wide margin, so it is not flaky. If
it fails, the training code has a bug.
6. Where it shows up #
- ML for science. Physical-consistency checks are behavioural tests: non-negative precipitation (an output range), conservation of mass or energy within a tolerance, invariance or equivariance under rotation or reflection where the physics has that symmetry (INV), and monotone responses such as "more rainfall gives more runoff" (DIR). Scoring a weather model per region, season and lead time is slice testing.
- Industry. A fraud model is sliced by country, payment method and merchant type; a recommender by new and old users. CheckList found many failures in commercial sentiment models that had high test accuracy (Ribeiro et al., 2020).
- Production. Slice and behavioural tests become the promotion gate in CI (lesson 03) and in the registry (lesson 06). The monitoring section of the ML Test Score is lesson 07 and lesson 08. LLM eval suites (lesson 09) are behavioural tests, sometimes with a model as the judge.
7. Common mistakes #
- Asserting exact float outputs of a trained model. They change with seeds, hardware and library versions. Assert properties with tolerances.
- Shipping on the aggregate metric alone. Check slices against production.
- A perturbation that changes the label. City names for a weather model, soil type for strong shaking. Check every perturbation with domain knowledge.
- Reading a passing behavioural suite as proof. It bounds a failure rate on the tested distribution only.
- Blocking on tiny slices. Use , a margin and a paired test.
- Comparing two models unpaired on the same rows. The unpaired test ignores that most rows agree, so it is too weak.
- Unseeded or slow model tests on every commit. Seed them, keep them tiny, and move long ones to a nightly or release job.
- Averaging the ML Test Score sections. The score is the minimum.
8. Check yourself #
- What is the difference between an invariance test and a directional expectation test?
Answer
INV applies a label-preserving perturbation and requires the score not to move by more than . DIR applies a perturbation with a known effect direction and requires the score not to move the other way (by more than ). Neither needs labels. - A slice of 40 rows drops by 3 accuracy points. Do you block the release?
Answer
Not on this alone. At accuracy near 0.75 the standard error on 40 rows is , so 3 points is about 0.4 standard errors. Report it; for a decision, use a paired test (McNemar) with a margin, or collect more rows. Set so that the slices you gate on can show the drops you care about. - Which test type catches each problem? (a) a feature computed with a timestamp after the label
time; (b) the serving image has another NumPy version and computes a feature differently;
(c) the model ignores negation.
Answer
(a) A data (leakage) test. (b) An infrastructure test: training-serving consistency, comparing online and offline features or predictions on logged requests. (c) A behavioural test, an MFT on negation. - How many independent passing cases do you need to claim a failure rate below 0.5% with 95%
confidence?
Answer
Solve : , so 598 cases. The rule of three gives . - On a slice, the old model is right and the new one wrong on 3 rows; the reverse happens on 9
rows. Is the new model worse on this slice?
Answer
No, it is better: , so the new model gains . McNemar: for gives , so the gain is also not clearly larger than noise. - Why is "the score must not change when we replace the station id" a bad INV test for a rain
model that takes the station id as an input?
Answer
The station id carries real information (altitude, local climate), so changing it can and should change the prediction. The perturbation is not label-preserving. INV tests on ids are valid only for ids that are not model inputs, such as a record id.
What is a minimum functionality test (MFT)?
Simple inputs with known labels, aimed at one capability (for example negation). A case fails if the thresholded score differs from the label.
What is an invariance test (INV)?
A label-preserving perturbation ; a case fails if . Example: change the reviewer's name.
What is a directional expectation test (DIR)?
A perturbation with known effect direction ; a case fails if . Example: lower pressure must not lower the probability of rain.
State the rule of three and its first proof step.
passing independent cases give the 95% upper bound . It comes from solving .
Compute the rule-of-three bound for 1,000 passing cases.
, about .
Why can a behavioural test suite not prove that a property holds?
It checks only the sampled inputs. A violation in a region that no case hits stays invisible; the suite only bounds the failure rate on its own case distribution.
Which rows does McNemar's test use?
Only the discordant rows: (old right, new wrong) and (old wrong, new right). Under no difference, .
What is the standard error of an accuracy measured on rows?
.
How is the final ML Test Score computed, and why?
The minimum of the four section sums (0.5 per manual test, 1 per automated test). The minimum makes a team cover all four areas to raise the score.
Which test type catches a feature computed with a timestamp after the label time?
A data test (a leakage check), run before training.
9. Interview angle #
- "How would you test an ML model before production?" Walk the table: code unit tests, data tests (contract and leakage), pre-training tests (overfit one batch), behavioural tests for the capabilities that matter, slice regression against production, infrastructure tests (the model loads, online = offline); then a canary.
- "The new model is better on the test set. Why not ship it?" A gain can hide a slice regression or a shortcut. Check slices with a paired test, run behavioural tests, check for leakage and for training-serving consistency.
- "How do you keep ML tests in CI from being flaky?" Seeds, tolerances, tiny data, statistical thresholds with margins, and slow suites outside the commit path.
- "Your behavioural suite passes. What does that tell you?" The failure rate on the tested case distribution is below about with 95% confidence. Nothing more.
10. Practice #
The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:
- Write
check_invarianceandcheck_directional, and catch a planted name shortcut in a bag-of-words review model whose test accuracy looks better than a clean model's. - Write an MFT on negation that the bag-of-words model fails, and explain why in one sentence.
- Write
slice_regressions, which flags slices with and a drop larger than , and adds McNemar's p-value. - Write
rule_of_three_bound(n, conf)and check it with a simulation and the exact binomial. - Write
ml_test_score(table)with the minimum rule and find the bottleneck section.
11. Watch and read #
- Ribeiro et al., Beyond Accuracy: Behavioral Testing of NLP Models with CheckList (ACL 2020), section 2: capabilities, the three test types, generating test cases at scale. The tool is open source: github.com/marcotcr/checklist.
- Breck et al., The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction (IEEE Big Data 2017): Tables I–IV list the 28 tests, section VI.A and Table V the scoring.
- Made With ML, Testing: tests for data, models (training tests, behavioural tests) and the difference between testing and monitoring.
- Wikipedia, McNemar's test and Rule of three (statistics).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.