Why software tests don't survive contact with LLMs
Twenty years of software engineering wisdom says: write a unit test, assert exact equality, run on every commit, fail the build on any regression. That entire muscle memory breaks the moment your function calls a probabilistic model.
- The same input produces different outputs across calls (sampling temperature, top-p, stochastic decoding).
- Even with temperature=0, model providers update model weights silently — the snapshot you tested last week may have shifted.
- The set of acceptable outputs is rarely a single string. "Paris is the capital of France." and "The capital of France is Paris." are both correct.
- Quality is graded, not binary. A 90% useful answer is materially different from a 60% one — both pass assert is_string(output).
What changes when you move to evals
| Aspect | Testing | Evaluation |
|---|---|---|
| Output | Deterministic | Stochastic |
| Pass criterion | Exact equality | Threshold on a graded score |
| Failure semantics | Bug | Regression in distribution |
| Retry policy | Flaky test | Statistical variance — run N times, average |
| Coverage | Lines / branches | Behaviors / edge cases / personas |
| Cost per run | Free (CPU) | Paid (LLM API calls × N samples × graders) |
Where they overlap — and where they don't
You should still write deterministic tests. Test that your retrieval function returns the right document IDs given a known query. Test that your tool-call parser raises on malformed JSON. Test that your prompt template substitutes variables correctly. Those are software bugs and software tests catch them. Evals layer on top, measuring the model's behavior given a correctly-wired system. Conflating the two — running an LLM call inside a unit test — produces flaky CI and angry teammates.