QASenior

Testing LLM products

Testing an LLM product means testing a feature whose model answers the same question differently on two runs, while most of its checks remain exact assertions. The page opens with twenty failures an order assistant ships with — the check that catches each, and where it runs — then covers the missing test oracle, the deterministic layer and its unhappy paths, properties for answers with no expected value, the 2026 OWASP list as a test plan, who owns what, and five releases that went wrong.

Exactly assertable
14 of 20 failures
No expected answer
Test a property
Adversary rows from
OWASP LLM Top 10, 2026

Twenty failures an LLM feature ships with — the check that catches each, and where it runs

Nobody opens a page about testing LLM products for a definition. They open it because a feature is due and the usual test plan does not fit: the output changes between runs, so what exactly does a test assert? The answer is less exotic than the question. Below are twenty failures an order assistant in an online shop is likely to ship with. It answers “where is my order”, explains the returns policy from the help centre and can start a return. Each row names what the customer gets, the check that catches it, what kind of check it is and when it runs. Rows 1–4 are the integration, 5–10 the output contract, 11–15 the content, 16–19 the adversary, and 20 is drift. Later sections cite them by number.

#What the customer getsThe check that catches itKind · when it runs
1An empty chat bubbleA recorded refusal and an empty answer both render a message and the offer of a personExact · every commit
2A spinner for thirty seconds, then “Something went wrong”An injected timeout, 429 and 500 from the provider: a handoff to a person inside the time budgetExact · every commit
3Half a sentence, or nothing: the reply was cut at the token limit and failed to parseA recorded max_tokens stop: the parser rejects it, and the feature retries or falls backExact · every commit
4“Your order has shipped” when the order service returned an errorAn injected tool error: no order status may appear in the reply without tool data behind itExact · every commit
5A reply the interface cannot renderSchema validation of every reply: the fields, their types, the allowed valuesExact · every commit, in production
6A ticket routed to a queue that does not existThe intent is one of the six allowed labelsExact · every commit
7A return started for the wrong orderTool-call arguments asserted: the order id belongs to this customer and is the one they namedExact · every change
8A refund amount that differs from the orderEvery number in the reply recomputed from the tool’s resultExact · every change
9A help-centre link to an article that says something elseThe cited article exists in the index and contains the quoted passageExact · every change
10A confident answer to a question the policy does not coverCases with no answer in the help centre: the handoff flag is setExact · every change
11A correct answer to a different questionA calibrated model judge: “does the reply answer the question asked — yes or no”Scored · every change
12A correct answer without the condition that decides it: the return window ends on FridayA judge given the facts each case requires: is every one of them presentScored · every change
13A different answer when the same question is asked in other words, or in UkrainianInvariance: paraphrases and translations of one case reach the same intent, tool call and factsProperty · every change
14The same decision when the facts change: “yes, you can return it” on day 34 of 30Direction: move the delivery date across the 30-day line, and the decision must flipProperty · every change
15An action taken because a product review or an email told the assistant toHostile text planted in the help centre and in tool output: the tool calls must match a run without itProperty · nightly, before release
16Another customer’s address in the replyEvery name, address and phone number in the reply belongs to the signed-in customerExact · every commit, in production
17The system prompt and the tool list read back to whoever asksA canary string from the prompt never appears in a reply — and nothing secret is in the prompt to begin withExact · before release
18A link or an image that carries data to another siteThe renderer allows only the shop’s own domains, in every Markdown form: inline, reference-style, imageExact · every commit
19One conversation that costs a hundred times the medianCaps on tool calls and tokens per conversation, tested with a case built to loopExact · every commit, in production
20Worse answers with nothing deployedA pinned model, the scored set run nightly against the live API, and production rates — refusals, handoffs, thumbs-down — with alertsLive · nightly, in production

What actually changes: the missing oracle

Testing research has a name for the thing that broke. A test oracle is whatever decides whether an output is correct — usually an expected value written next to the input. In 2015 Earl Barr, Mark Harman and colleagues surveyed the oracle problem: how to tell correct behaviour from incorrect when that expected value is missing, or too costly to write. Scientific simulations have it, machine-learning models have it, and an LLM feature has it in its purest form. The expected output of “where is my order?” is any one of a thousand acceptable sentences.

What did not break is the rest of the idea. An oracle does not have to be an expected string. It can be a contract the output must satisfy, a relation between two outputs, a grader that returns a verdict, a person, or what real users did with the answer. Every row of the table above uses one of those, and the last column says which.

OracleWhat it decidesCost per runRows
A contract (exact)The output obeys a rule: it parses, has its fields, its number matches the tool, its link is on our domainNothing; milliseconds1–10, 16–19
A relation (property)Two outputs relate as they should: the same for a paraphrase, different when a fact changesTwo calls of the feature13–15
A grader (scored)A model returns a verdict on one output against a written criterionA call per case, plus calibration11–12
A personReads a sample before release, and labels the cases every grader is calibrated againstMinutes per casebehind 11–12
Production (live)The real mix of inputs, and what users did with the answersNothing per run; slow to read20
The whole difficulty of testing an LLM feature fits in one change. On the left is the test you already know: the same input produces the same output, so the assertion is an equality and the verdict is binary. On the right the same input produces a different answer on every run, so an equality is not merely strict — it is meaningless. The assertion becomes a grader that returns a number and a threshold that decides, and a single run stops being evidence: you look at a rate across many cases rather than a pass on one. What the row underneath adds is the half that gets forgotten in the excitement. A great deal about an LLM feature is still exactly assertable — that the response parses as valid JSON matching your schema, that no personal data appears in it, that the expected tool was called with the expected arguments, that it arrived inside the latency and cost budget. Those checks are instant, free and unarguable, and they should carry as much of your suite as they possibly can before any score is involved.

A common shortcut is to set the temperature to zero and assert the string anyway. Temperature zero means the model always takes its most likely next token, which should make the output repeatable. In September 2025 Horace He and colleagues at Thinking Machines Lab showed why it is not on an ordinary inference server. They sent one prompt a thousand times at temperature zero and got eighty different completions. All thousand agreed for the first 102 tokens; at the 103rd, 992 continued “Queens, New York” and eight “New York City”.

The cause was not randomness in the model but load on the server. How many other users’ requests were batched together changed the order of floating-point additions, and with it the last digits that settle a near tie between two tokens. Their fix, batch-invariant kernels, made all thousand completions identical, at a cost in speed. A shared does not run it for you. So treat a hosted model as non-deterministic whatever the temperature, and write tests that do not care.

The deterministic layer first: contract, tools and unhappy paths

Draw the feature as boxes and the shape of the work is obvious. The order assistant is a request handler, an intent router, a search over help-centre articles, a model call, a parser, three tools and a renderer. One box is probabilistic. The rest are ordinary code, and each carries exact checks that run in milliseconds. The diagram pins every row of the table to the box it tests.

Drawn as boxes, the order assistant is mostly ordinary software. A request comes in, a router picks one of six intents, a search finds help-centre articles, the model writes a reply and may call one of three tools, a validator checks the result and a renderer shows it. Only the model is probabilistic. Each of the page’s twenty rows sits under the box it tests. Fourteen are exact checks and they cover six of the seven boxes: the intent label at the router, citations at the search, tool arguments and failures at the tools, schema, numbers, the handoff flag, personal data and the prompt canary at the validator, refusals and outbound links at the renderer. Even the model box carries one exact row — row 2, the provider call timing out — next to the two scored rows and the one that can only be seen in production. The three property rows sit at the request, where the input is varied, and at the search, where hostile text is planted.

Structured outputs removed one whole class of these failures and left three. When OpenAI introduced them in August 2024, its own evaluation of complex schemas went from under 40% for an older model to 100% with strict mode on. The same announcement lists what the guarantee does not cover. A refusal comes back as a refusal instead of the schema. A reply that reaches max_tokens stops before the object is complete. And the values inside a valid object can still be wrong. Those three limits are rows 1, 3 and 8, which is why a schema guarantee does not retire the tests — it tells you which ones to write.

The integration rows can run on every commit only if they cost nothing, and they cost nothing only if the provider is not called. So record real responses once — an ordinary answer, a refusal, a truncated reply, a — and replay them from files in the test run. Then write the responses nobody can record on demand: a timeout, a 429, a 500, a tool that fails, a reply with broken . The suite is now fast and deterministic, and it tests exactly the code that turns provider behaviour into what a customer sees.

The integration rows can run on every commit only if no provider is called, so the responses come from files. Some are recorded from the live provider — an ordinary answer, a refusal, a reply cut off at the token limit, a tool call. The rest are written by hand, because nobody can make a provider time out, return 429 or 500, send broken JSON or have a tool fail on demand. Every commit replays these files into your own code, and the exact rows 1 to 6 assert on what the customer would see; the run is free, takes seconds and gives the same result every time. What is under test is your code, not the provider. That is also its weakness: the recordings show the provider as it was on the day they were made. The nightly run against the live API, row 20, is what notices when the two have drifted apart, and the recordings are then the first thing to suspect.
FaultWhat the customer must getRow
The provider times outA message and a handoff inside the time budget — not a spinner2
429, rate limitedOne retry with backoff, then the handoff; batch jobs never take the chat’s quota2
Stopped at max_tokensRejected by the parser; one retry asking for a shorter answer3
A refusalA sentence the interface can show, never an empty box1
The order tool fails“I can’t see your order right now” — never a status4
Broken JSONRejected, retried once, and counted in the schema-failure rate5

No expected answer? Test a property

Rows 13–15 have no expected output, and they are still not judged by a score. They compare two runs of the feature with each other. Software testing has done this for decades wherever an oracle is missing. Metamorphic testing, reviewed by Tsong Yueh Chen and colleagues in 2018, checks a relation between outputs instead of the outputs themselves: rotate the input image, and the detected objects should rotate with it.

In 2020 Marco Tulio Ribeiro and colleagues brought the idea to language models as CheckList and named three test types. A minimum functionality test is a small set of simple cases that must all pass. An invariance test changes the input in a way that must not change the output. A directional test changes it in a way that must move the output in a known direction. In their user study, practitioners with CheckList wrote twice as many tests and found almost three times as many bugs as those without it.

Test typeChange to the inputWhat must holdOn the order assistant
Minimum functionalityNone — simple casesEvery case passes“Where is order 1042?” calls get_order with 1042; “I want to return the blue jumper” opens a return
InvarianceA paraphrase, another language, a typo, a sentence about the weatherThe same intent, tool call and factsRow 13
Invariance of the actionA hostile instruction inside a review or an emailThe same tool calls as the run without itRow 15
DirectionThe delivery date moved from day 30 to day 31The return decision flips to noRow 14
DirectionThe order number removedThe assistant asks for it instead of guessingRow 7, from the other side
A property test needs no expected answer. It starts from one source case — can I return order 1042, delivered twelve days ago — whose output is yes and a call to start the return. Rules then make follow-up cases from it, and each follow-up carries the relation its output must have to the source’s rather than an answer of its own. A paraphrase and a translation must reach the same decision; that is invariance. Moving the delivery date to day 31, past the thirty-day line, must flip the decision to no; that is a directional test, and it is boundary-value testing in a new setting. A review that tells the assistant to ignore the policy and refund must leave the tool calls exactly as they were without it; that is invariance of the action, the test for injected instructions. None of the four needs a grader, and one source case can produce many follow-ups by rule.

A property needs no labelled answer, which is what makes it cheap to scale: one source case becomes ten follow-up cases by rule. Its limit is that both runs can be wrong in the same way. A question and its paraphrase can agree perfectly on a return window the policy never stated. That is why properties sit beside the scored rows rather than replacing them.

The adversary: the 2026 OWASP list as a test plan

Rows 15–19 come from a list a tester can use directly. published the 2026 edition of its Top 10 for LLM Applications on 4 August 2026. For the first time the ranking is not opinion alone: the practitioners’ vote carries three quarters of the weight, and 6,639 classified real incidents carry the rest. The preface states the posture behind all ten entries: “Stop trying to build a model that cannot be fooled. Build the system around it, so that when the model is fooled, and it will be, nothing important breaks.”

For a tester that sentence is the plan. You do not test whether the model can be tricked; it can. You test what the system lets a tricked model do — which tools it can reach, what it can show, what it can send and how much it can spend.

EntryOn the order assistantThe testRow
LLM01 Prompt InjectionText in a review, an email or a tool result is read as an instructionHostile documents planted in the index and in tool output; the actions compared with a clean run15
LLM02 Sensitive Information DisclosureAnother customer’s data or internal notes in a reply — or in tool arguments and logsEvery personal detail in the output checked against the signed-in account; the same scan over traces16
LLM03 Excessive AgencyThe assistant can do more than a conversation needs — refund, not just start a returnExactly three tools exist; a test that no prompt reaches anything else—
LLM06 Unbounded ConsumptionA conversation that loops through tools or asks for an essayCaps per conversation, and a case built to loop19
LLM07 MisinformationA policy stated confidently and wrongly, then acted onCitations that must contain the claim, cases with no answer, required facts9, 10, 12
LLM08 Hidden Context ExposureThe system prompt and tool schemas read back to a curious customerA canary string; nothing in the prompt that would matter if it were read17
LLM10 Improper Output HandlingThe reply is rendered without checks: links, images, HTMLRenderer tests for every Markdown form, not only the inline one18

The other three entries — supply chain (LLM04), data and model poisoning (LLM05) and vector and embedding weaknesses (LLM09) — live mostly in how the model and the index are built and fed. They belong to a security review of that pipeline more than to the feature’s test suite. One practical trap: the 2026 list moved eight of its ten entries. System Prompt Leakage, LLM07 in 2025, is now Hidden Context Exposure, LLM08; Improper Output Handling fell from fifth to tenth. A test plan that cites entries by number alone now points at the wrong ones, so cite the year with the number — LLM08:2026.

Who writes what, and where each check runs

A tester on an ordinary feature writes steps and expected results. On an LLM feature the steps are trivial — send this, read that — and the difficulty moves into defining what a good answer is. That definition is a specification. It is genuinely hard to write, and it is the most valuable artefact on the project, because every scored row and every grader is only as good as it.

  • QA owns the definition of good

    The written rubric: what must be present, what must never be, what tone, what to do when the answer is not known. Without it every review is an argument about taste.

  • QA owns the failure corpus

    Every complaint, incident and surprising answer becomes a case. Curating it is what makes the suite better exactly as the product gets used.

  • Engineering owns the harness

    Recording and replaying responses, pinning versions, storing results, wiring the gate into the pipeline. Ordinary infrastructure, and it should be treated as such.

  • One named owner for the number

    Somebody must be able to block a release on quality. An LLM feature has no natural owner: the prompt, the search and the invoice usually belong to three different people.

A domain expert belongs in this loop and is usually missing from it. Whether a refund rule is stated correctly, whether a medical instruction is safe, whether a reply follows the company’s policy — the team that built the feature cannot settle these, and a rubric written without the expert encodes the engineers’ guess about the domain. Book the expert’s time early and explicitly, and treat their labels as the reference every grader is calibrated against.

MomentWhat runsWhy there
Every commitThe exact rows against recorded responses: 1–6, 16, 18, 19.Free and instant, and it covers the failures that cause most incidents.
A change to the prompt, the model or the indexRows 7–14 on the live model: the scored set, compared case by case with the recorded baseline.These three are the deployable artefact. A change to any of them is a release, whatever the ticket says.
NightlyThe same set against the live API, plus the adversarial cases of row 15.Catches drift you did not cause: a provider update, a new refusal shape, a slower endpoint.
Before a releaseThe protected set — the cases nothing may break — rows 15 and 17, and a person reading a sample.The last moment a person looks at real output before a customer does.
In productionRows 5, 16, 19 and 20 as monitors: schema failures, personal data, cost per conversation, refusal and handoff rates, thumbs-down.The only place the real mix of inputs exists. Your test set is always a sample of last quarter.

Five releases, and the rows that would have caught each

None of these was a model that could not be tested. In each, something reached the public that one of the twenty rows checks for, and in every one the people who found it were users, journalists or outside researchers rather than the team’s own suite.

CaseWhat happenedThe rows that cover it
Tessa, NEDA, May 2023A chatbot for people with eating disorders, built by specialists from preprogrammed replies, was given generative AI by its operator as part of a systems upgrade. A consultant, Sharon Maxwell, asked it about healthy eating and was told to keep a calorie deficit of 500 to 1,000 a day. She says it was taken down within hours of her screenshots; the association had closed its human helpline the month before.20 and 12. Swapping scripted replies for a generative model is a release that needs the whole suite again, and a specialist’s list of what must never be said is the scored set that would have failed the first reply.
Gemini images, Feb 2024Tuning meant to show a range of people was applied to prompts that named a specific kind of person or a historical setting, and over time the model became, in Google’s words, “way more cautious than we intended”, refusing harmless prompts. Google paused image generation of people three weeks after launch.14 and 1. A directional case — a prompt that specifies who must get what it specifies — and the refusal rate tracked as a number, not discovered as a mood.
Apple notification summaries, Dec 2024Summaries of news alerts, shown under the publishers’ names, stated things the articles did not — among them that the man accused of killing UnitedHealthcare’s chief executive had shot himself. The BBC complained in December; on 16 January 2025 Apple switched the summaries off for news and entertainment apps in the iOS 18.3 betas.9 and 12. A summary is checked against its source — every claim in it must be in the article — and a set of real headlines is the scored set.
Grok, May 2025At about 3:15 am Pacific time on 14 May, someone changed the X bot’s system prompt to give a “specific response” on a political topic, and Grok inserted it into replies about unrelated subjects. xAI called it an unauthorised modification, began publishing its prompts on GitHub and said changes would need review.13 and 20. Replies to unrelated questions must not share a topic — an invariance case — and a prompt change is a deployment, reviewed, with the protected set run before it.
EchoLeak, Microsoft 365 Copilot, Jun 2025An email from outside the company carried instructions written as if to its human reader, so the injection classifier let it through. When Copilot later retrieved it, the reply could carry data out through reference-style Markdown links and images, which the link redaction did not recognise. CVE-2025-32711, rated 9.3. It was fixed before the write-up appeared, and Microsoft says no customers were affected.15 and 18. A hostile document in the index, and a renderer tested with every Markdown form — not only the one everyone writes.

Where it degrades

The honest limit of everything on this page is that a suite checks what somebody thought to put in it. Twenty rows are twenty failures someone imagined; property tests need someone to invent the relation; a red team finds the attacks of its own members. Testing cannot show that an injection is impossible, which is exactly why the OWASP preface asks for a system in which a fooled model breaks nothing important — and why the rows that limit what the model can do (7, 16–19) are worth more than any score.

  • Declaring the feature untestable and shipping on demos, which turns every customer into an unpaid tester with no way to report.
  • Asserting exact strings, which gives a suite that fails on every run for reasons unrelated to quality — and gets switched off.
  • Setting the temperature to zero and calling the output deterministic.
  • Starting with the scored rows because they are new, and never reaching the exact ones that cause the incidents.
  • A replay suite that has not been re-recorded since launch, green against a provider that no longer exists.
  • A red-team exercise run once, its findings fixed and never turned into cases.
  • Prompt edits and vendor upgrades that skip the suite because “nothing was deployed”.
  • OWASP entries cited by number without the year, after the 2026 renumbering.
  • Leaving cost and latency out of the suite, so the first person to notice is whoever opens the invoice.

When to use it

Use it when

  • Any feature where a model’s output reaches a customer, another system or a decision with consequences.
  • Start with the exact rows on recorded responses — the integration, the contract, the adversary — before any scored evaluation.
  • Use properties where no answer can be written down: paraphrase, language, boundary values, hostile text.
  • Treat a prompt edit, a model upgrade and a vendor change as releases that run the suite.

Avoid it when

  • Exact-string assertions on generated text, which fail constantly for reasons that have nothing to do with quality.
  • Conclusions drawn from one run, where sampling alone can support either answer.
  • A model-graded score where a schema check, a recomputation or a lookup would settle it exactly.
  • Testing whether the model can be fooled instead of what a fooled model is able to do.

Sources and method

What this page stands on

  • 13 sources
  • 5 documented failures
  • Checked October 17, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.