- Posted on
- • Artificial Intelligence
Artificial Intelligence Testing with pytest
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Testing with pytest: Make Your Models Trustworthy on Linux
Ever shipped a model that scored 92% locally, only to see it dip to 88% in CI a week later? Or had a “flaky” test that passed yesterday and fails today—without any code changes? AI systems are uniquely sensitive to randomness, floating-point quirks, data drift, and performance regressions. The fix isn’t a new framework—it’s disciplined, automated testing with pytest.
In this guide, you’ll learn why AI testing is different, how pytest fits perfectly into a Linux-based workflow, and you’ll walk away with practical, reusable test patterns you can drop into your repos today.
Why test AI with pytest?
Reproducibility beats surprises: Seed control and deterministic pipelines stop “works on my machine” issues.
Guarantees over guesses: Tolerance-based assertions let you check correctness without demanding bit-for-bit equality.
Defend your SLAs: Benchmark inference and catch latency regressions before your users do.
Ship with confidence: Automated tests integrated into CI guard your accuracy and data contracts over time.
Quick install: system packages and Python tooling
You’ll want Python 3, venv, pip, a compiler toolchain, and then pytest plus a few helpful plugins.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y python3 python3-venv python3-pip build-essential gcc
Fedora/RHEL/CentOS (dnf):
sudo dnf install -y python3 python3-pip python3-virtualenv gcc gcc-c++ make
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y python3 python3-pip python3-virtualenv gcc gcc-c++ make
Create and activate a virtual environment:
python3 -m venv .venv
. .venv/bin/activate
Install Python packages:
pip install --upgrade pip
pip install pytest pytest-cov pytest-benchmark pytest-xdist pytest-rerunfailures numpy scikit-learn
Optional, if you use pandas or PyTorch:
pip install pandas torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
A minimal, realistic AI example
Let’s keep the example small but real: an Iris classifier using scikit-learn with a pipeline for preprocessing + model.
Create ai_model/model.py:
from dataclasses import dataclass
from typing import Tuple
import numpy as np
from sklearn.datasets import load_iris
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
@dataclass
class TrainResult:
model: object
X_test: np.ndarray
y_test: np.ndarray
accuracy: float
def load_data() -> Tuple[np.ndarray, np.ndarray]:
data = load_iris()
return data.data.astype(np.float64), data.target.astype(np.int64)
def build_pipeline(random_state: int = 0):
# StandardScaler + LogisticRegression is deterministic with liblinear + seed
return make_pipeline(
StandardScaler(),
LogisticRegression(random_state=random_state, solver="liblinear", max_iter=1000)
)
def train(random_state: int = 0) -> TrainResult:
X, y = load_data()
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, random_state=random_state, stratify=y
)
pipe = build_pipeline(random_state=random_state)
pipe.fit(X_tr, y_tr)
y_hat = pipe.predict(X_te)
acc = accuracy_score(y_te, y_hat)
return TrainResult(model=pipe, X_test=X_te, y_test=y_te, accuracy=acc)
def predict(model, X: np.ndarray) -> np.ndarray:
# Return class probabilities to make tolerance-based checks meaningful
return model.predict_proba(X)
5 actionable testing patterns that work for AI
1) Make randomness reproducible everywhere
Seed Python, NumPy, and your ML libraries inside pytest fixtures. Also disable multi-thread nondeterminism in CI via environment variables.
Create tests/conftest.py:
import os
import random
import numpy as np
import pytest
@pytest.fixture(autouse=True)
def seed_all():
# Keep Python hashing stable across runs
os.environ["PYTHONHASHSEED"] = "0"
# Limit threaded math libraries for determinism (CI-friendly)
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("MKL_NUM_THREADS", "1")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
seed = 0
random.seed(seed)
np.random.seed(seed)
try:
import torch
torch.manual_seed(seed)
torch.use_deterministic_algorithms(True, warn_only=True)
except Exception:
pass
yield
Bonus: In CI, add this to your job’s shell step:
export PYTHONHASHSEED=0 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 NUMEXPR_NUM_THREADS=1
2) Test your data contracts and preprocessing
Catch schema and range issues early—before training silently degrades.
Create tests/test_data_contract.py:
import numpy as np
from ai_model.model import load_data, build_pipeline
def test_load_data_shapes_and_types():
X, y = load_data()
assert X.ndim == 2 and y.ndim == 1
assert X.shape[0] == y.shape[0]
assert X.dtype == np.float64
assert y.dtype in (np.int32, np.int64)
assert not np.isnan(X).any()
def test_pipeline_contains_scaler_and_classifier():
pipe = build_pipeline()
names = [name for name, _ in pipe.steps]
assert "standardscaler" in names
assert "logisticregression" in names
3) Use tolerance-based assertions, not exact equality
Floating-point math and randomized algorithms won’t be bit-exact. Use relative/absolute tolerances for probabilities, coefficients, and metrics.
Create tests/test_training_and_metrics.py:
import numpy as np
from ai_model.model import train, build_pipeline, load_data
def test_training_is_reproducible():
# Train twice with the same seed and compare coefficients within tolerance
r1 = train(random_state=0)
r2 = train(random_state=0)
# Extract coefficients from logistic regression inside the pipeline
clf1 = r1.model.named_steps["logisticregression"]
clf2 = r2.model.named_steps["logisticregression"]
assert np.allclose(clf1.coef_, clf2.coef_, rtol=1e-6, atol=1e-8)
assert np.allclose(clf1.intercept_, clf2.intercept_, rtol=1e-6, atol=1e-8)
def test_accuracy_threshold_guardrail():
# If this trips, something changed: data, preprocessing, or model
r = train(random_state=0)
assert r.accuracy >= 0.90
4) Keep small “goldens” to catch semantic regressions
Golden outputs (small arrays, JSON blobs) catch subtle behavior changes. Store just a few rows to keep it light.
Create tests/test_golden_inference.py:
import os
import numpy as np
import pathlib
import pytest
from ai_model.model import train, predict
GOLDEN = pathlib.Path(__file__).parent / "goldens" / "probas_first5.npy"
@pytest.mark.filterwarnings("ignore::numpy.VisibleDeprecationWarning")
def test_inference_matches_golden_with_tolerance(tmp_path):
r = train(random_state=0)
probas = predict(r.model, r.X_test)
# Focus on first 5 rows to keep goldens tiny
first5 = probas[:5]
# If golden missing, create it once intentionally (local dev convenience).
# In CI, the golden should exist and be version-controlled.
if not GOLDEN.exists():
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
np.save(GOLDEN, first5)
pytest.skip("Golden created; re-run tests to validate.")
golden = np.load(GOLDEN)
assert first5.shape == golden.shape
# Tolerances account for minor numeric drift but catch meaningful changes
assert np.allclose(first5, golden, rtol=1e-4, atol=1e-6)
Tip:
Commit
tests/goldens/probas_first5.npyto your repo.If you knowingly change behavior (e.g., new preprocessing), regenerate and review the diff.
5) Measure and lock down performance with pytest-benchmark
Latency is a feature. Track inference speed to spot slowdowns.
Create tests/test_performance.py:
import numpy as np
import pytest
from ai_model.model import train, predict
@pytest.mark.benchmark(group="inference")
def test_inference_latency_benchmark(benchmark):
r = train(random_state=0)
X = r.X_test
def run():
return predict(r.model, X)
# The benchmark plugin stores timing; you can compare across commits with
# --benchmark-compare or maintain a baseline. Avoid hard thresholds here.
result = benchmark(run)
assert result.shape[0] == X.shape[0]
Run locally:
pytest -q
pytest -q --benchmark-only
pytest -q --benchmark-compare
Parallelize tests and tame flakiness when needed:
pytest -q -n auto --reruns 2 --reruns-delay 1
Generate reports for CI:
pytest -q --cov=ai_model --cov-report=term-missing --junitxml=reports/junit.xml
Useful bash one-liners for CI
- Fail fast if unseeded randomness sneaks in:
export PYTHONHASHSEED=0 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 NUMEXPR_NUM_THREADS=1
pytest -q -n auto --maxfail=1
- Cache dependencies and run benchmarks only on main:
if [ "$GITHUB_REF_NAME" = "main" ]; then
pytest --benchmark-only --benchmark-save=main
else
pytest --benchmark-only --benchmark-compare=main
fi
Real-world tips
Keep tests small: Use tiny datasets or sample production data. Avoid GPU in unit tests; keep that for integration/perf suites.
Mock external services: For LLM calls or HTTP-dependent steps, use pytest’s
monkeypatchor libraries likeresponsesto avoid network flakiness.Separate test tiers: Fast unit tests on every commit, heavier benchmarks and integration tests on schedule or on main.
Version data and models: Pair your tests with data snapshots (DVC, git-lfs) for real reproducibility.
Conclusion and next steps
AI testing isn’t optional—your users will test in production if you don’t. With pytest on Linux, you can:
Make runs reproducible,
Assert correctness with sensible tolerances,
Guard your accuracy and latency over time,
And integrate it all cleanly into CI.
Your next step:
1) Drop the snippets above into your repo.
2) Wire up pytest -q --cov=... in your CI.
3) Add one golden test and one benchmark.
4) Watch flakiness and regressions disappear.
Have a tip or a gnarly edge case? Share it—and let’s make AI testing as routine (and boring) as any other software test.