Evals for Coding Agents
Eval Basics · Chapter 4

Evals vs. Classic QA — and Building a Scorecard a Team Actually Trusts

Chapters 1–3 built the Golden Dataset machinery. This chapter answers the three questions that naturally follow: what actually separates an eval from the unit/integration/UAT/regression testing engineers already know? Can JUnit and Pytest still do the job? And how does a scorecard turn into something a whole team trusts and watches, with real thresholds for "good enough to ship"?

Blue — core concept
Red — the problem / pitfall
Green — takeaways & fixes
Yellow — definitions & highlights

← Back to All Chapters

Table of Contents

1. Classic QA vs. Evals: What Actually Changes

Every classic QA discipline shares one load-bearing assumption: given the same input and unchanged code, the system produces the same output, every time. Unit testing, integration testing, UAT, and regression testing are all really the same idea applied at different scopes: write down the one correct answer, compare the system's output to it, and treat any mismatch as a bug. That works perfectly for deterministic code — and it's exactly the assumption Chapter 1 showed doesn't hold for an LLM or an agent.

DimensionClassic QAEvals
Core assumptionDeterministic: same input → same output, alwaysProbabilistic: same input can yield different, still-valid outputs
Pass/failBinary — exact match against one expected valueScored — graded against a rubric or multiple valid answers, often as a rate across many runs
What one test run tells youDefinitive, given unchanged codeOne sample from a distribution — not proof of anything on its own
What triggers investigationA previously-passing test now failsA pass rate across many runs drifts outside its normal range
Likely cause of a failureA code bug — the root cause is assumed deterministicCould be a real bug, model version drift, sampling variance, or an underspecified test case
Unit of measurementTest count — "342 of 350 passing"A metric with a distribution — "recall 91% ± 4% over 50 runs"
Reference dataHandwritten exact expected outputA Golden Dataset — verified ground truth plus rubrics
Rerunning the same testRedundant by designNecessary — it's the only way to measure reliability at all
"Is it fixed now?"Yes/no — the test goes greenStatistical — did the score move more than normal noise, or not?
This doesn't retire classic QA — it draws a boundary around where it still applies

Go back to the Woody's Hardware validator from Chapter 3. validator.js is 100% deterministic code — no LLM anywhere in it. That file is still exactly as unit-testable as it always was. A classic Pytest or JUnit suite asserting validateApplication(GD_001_INPUT).eligible === true is completely appropriate — in fact, it's how you'd validate that the Golden Dataset's own expected outputs are correct in the first place.

What's new is the layer sitting on top of that deterministic code: an agent asked to read validator.js and predict its behavior, or apply the 20 written rules itself without seeing the code. The rule of thumb: deterministic code keeps its classic QA suite; the probabilistic judgment layered around or on top of it needs evals. Most real agent-based systems are a mix of both.

↑ Back to Table of Contents

2. Reusing JUnit and Pytest for Evals

Here's the good news: you don't need new tooling to start running evals. JUnit and Pytest's actual value was never "exact-match assertions" — it was test discovery, fixtures, parameterization, CI integration, and reporting. All of that infrastructure is just as useful for evals. What changes is what you assert, and how you interpret a failure.

Technique 1 — Parameterize over the Golden Dataset

Pytest's @pytest.mark.parametrize and JUnit 5's @ParameterizedTest were built to run one test body across many inputs — exactly what a Golden Dataset needs.

test_agent_eval.py
import pytest
from golden_dataset import GOLDEN_CASES          # the 10 entries from Chapter 3
from agent_client import ask_agent_to_validate
from eval_metrics import rule_level_recall, rule_level_precision, agreement_rate

@pytest.mark.parametrize("case", GOLDEN_CASES, ids=lambda c: c["id"])
def test_agent_matches_ground_truth(case, eval_report):
    runs = [ask_agent_to_validate(case["input"]) for _ in range(5)]   # 5 samples per case

    avg_recall = sum(rule_level_recall(r, case["expected"]) for r in runs) / len(runs)
    avg_precision = sum(rule_level_precision(r, case["expected"]) for r in runs) / len(runs)
    consistency = agreement_rate(runs)

    eval_report.record(case["id"], recall=avg_recall, precision=avg_precision, consistency=consistency)

    assert avg_recall >= 0.90, f"{case['id']}: recall {avg_recall:.0%} below threshold"
    assert consistency >= 0.80, f"{case['id']}: consistency {consistency:.0%} below threshold"
Technique 2 — Repeat runs to measure self-consistency

JUnit 5's @RepeatedTest was designed to help surface flaky tests. It turns out to be a near-perfect fit for measuring an eval's self-consistency instead.

AgentEvalTest.java
@RepeatedTest(5)
void agentFindsKnownBankruptcyFailure(RepetitionInfo repInfo) {
    AgentResult result = agentClient.validate(GD_004_INPUT);
    consistencyTracker.record("GD-004", repInfo.getCurrentRepetition(), result);

    assertTrue(result.failedRules().contains("R10"),
        "Run " + repInfo.getCurrentRepetition() + " missed the bankruptcy rule (R10)");
}

@AfterAll
static void checkConsistency() {
    double agreement = consistencyTracker.agreementRateFor("GD-004");
    assertTrue(agreement >= 0.80, "GD-004 consistency " + agreement + " was below the 80% threshold");
}
Technique 3 — Assert on a threshold, not an exact value

assertEquals(expected, actual) fails the instant one value doesn't match. Evals instead accumulate a score across many runs and assert that the aggregate clears a bar — closer to assertTrue(recall >= 0.90) than to exact equality. Pytest's pytest.approx and JUnit's soft-assertion patterns are useful low-level primitives here, but the real shift is conceptual: you're asserting on a statistic, not a value.

Technique 4 — Use fixtures/extensions for the bookkeeping evals specifically need

A Pytest fixture (or a JUnit TestWatcher extension) that automatically records the model version, temperature, prompt version, and timestamp alongside every result is exactly the kind of cross-cutting concern this tooling was built for — and it's what makes results reproducible later (Chapter 1).

Technique 5 — Keep the CI integration you already have

Because it's still fundamentally a JUnit or Pytest suite, it plugs into the CI pipeline, the JUnit-XML report format most dashboards already consume, and the failure-notification tooling your team already trusts — without standing up new infrastructure from scratch.

The one mindset shift that actually matters

In classic QA, a flaky test — one that passes sometimes and fails sometimes with unchanged code — is treated as a bug in the test to be hunted down and eliminated. In evals, some of that same pass/fail variance is not a defect to eliminate — it's the expected, inherent behavior of the system under test (Chapter 1). The job changes from "stamp out flakiness" to "measure and bound flakiness." That inversion is the single biggest adjustment a QA engineer has to make moving into evals.

↑ Back to Table of Contents

3. From Scorecard to Trusted Team Practice

Chapter 3 ended by generating a scorecard: overall accuracy, rule-level precision/recall, and a consistency score. A number sitting in a log file doesn't earn anyone's trust. Here's what turns it into something a team actually watches and believes.

Dashboard elementWhy it matters
Current score per metricThe baseline "where do we stand right now" view
Per-category / per-rule breakdownAn overall average can stay flat while one rule (e.g. R8's DTI check) silently drops from 95% to 60% recall — only a breakdown catches that
Trend lines over time / versionsTurns "we think the new version is better" into a chart anyone can look at
Confidence context, not false precision"87% ± 4% over 50 runs" is honest; a bare "87%" implies certainty the sample size doesn't support
Comparison against a baselineMost decisions are relative — "is this better or worse than what's live" — not about hitting an abstract ideal
Drill-down to actual failing runsA number tells you that something regressed; the transcript tells you why
Setting thresholds
  • Different rules deserve different bars. A rule tied to compliance or safety exposure — citizenship eligibility, consent, bankruptcy — deserves a much higher required recall than a cosmetic completeness check.
  • Two tiers: hard gates and soft thresholds. A hard gate automatically blocks a merge/release when a critical metric falls below its bar; a soft threshold flags a metric for human review without blocking anything.
  • Check against the noise floor before calling something a regression. A 3-point drop might be real or might be well within normal run-to-run spread — you need to know your metric's typical noise level first.
  • This is structurally closer to an SLO than to a pass/fail gate. Site reliability engineering doesn't demand 100% uptime; it sets a target and an error budget, then treats sustained drift as the trigger for action.
What "good enough to ship" actually means for an AI agent

This is not "100% pass rate" — that's a classic-QA instinct that doesn't transfer.

  1. It's a risk-calibrated band, not one global number.
  2. It's usually relative to a baseline, not an absolute ideal — "at least as good as what's already shipping" (non-regression).
  3. It's never a one-time gate — it's continuously monitored after shipping, because the underlying model can drift with zero code changes on your end (Chapter 1).
  4. It needs an owner and a review process, not just a metric. A team trusts a scorecard once there's a known answer to "who looks at this when a metric turns red, how often, and what happens next."
MetricHard gate (blocks shipping)Soft threshold (flags for review)
Rule-level recall on compliance-sensitive rules (R4, R10, R13, R20)≥ 99%—
Overall eligibility-verdict accuracy≥ 95%≥ 98% target
Rule-level recall, all other rules≥ 90%≥ 95% target
Self-consistency (agreement across 5 runs, per case)≥ 85%≥ 95% target
Change vs. last shipped version, any metric aboveNo drop greater than the measured noise floorAny drop at all, even within noise

Notice the asymmetry: rules with real business/regulatory consequences get a near-100% hard gate; everything else gets a looser bar with a softer, review-triggering threshold.

↑ Back to Table of Contents

4. Key Takeaways

  • Classic QA is built on one assumption — same input, unchanged code, same output — that simply does not hold for LLM-based agents.
  • The fix isn't to discard classic QA: deterministic code still deserves classic unit tests; evals are for the probabilistic, judgment-driven layer on top of it.
  • JUnit and Pytest remain useful for evals — parameterization, repetition, fixtures, and CI integration all transfer directly. What changes is asserting on a threshold/statistic across many runs instead of an exact match.
  • The biggest mindset shift: a classic flaky test is a bug to eliminate; eval variance is expected behavior to measure and bound, not stamp out.
  • A trustworthy dashboard needs per-category breakdowns, trend lines, confidence context, baseline comparisons, and drill-down to real examples.
  • Thresholds work best as tiered, risk-calibrated SLO-style bars checked against a known noise floor — not one blanket pass/fail line.
  • "Good enough to ship" is a risk-calibrated band relative to a baseline, continuously monitored after shipping — never a one-time 100%-pass gate.

↑ Back to Table of Contents

5. Glossary

TermMeaning
Flaky testIn classic QA, a test that passes or fails inconsistently with unchanged code — treated as a defect in the test itself.
Noise floorThe normal amount an eval metric moves run-to-run even with no real change.
Hard gateA threshold that automatically blocks a merge/release when crossed.
Soft thresholdA threshold that flags a metric for human review without blocking anything.
SLO (Service Level Objective)A target reliability bar paired with an error budget, rather than a demand for zero failures.
Ship gateThe specific set of metrics and thresholds a team agrees must be cleared before a given agent/model version is allowed to ship.
Regression (eval sense)A statistically meaningful drop in a score across runs — distinct from "a test that used to pass now fails."

↑ Back to Table of Contents

This is Chapter 4 in the Eval Basics series. Next: the common pitfalls that quietly invalidate an eval even when the dashboard looks healthy — overfitting to your own Golden Dataset, Goodhart's Law, and how to keep a benchmark honest. — continue to Chapter 5: Goodhart's Law →