The test checked whether the extracted invoice total matched the expected value. It passed nine times out of ten. On the tenth run, the model formatted the number differently — "1,234.56" instead of "

Markdown supported · ⌘↵ to post
Good breakdown of the tradeoffs. One middle ground worth mentioning: for the live-API integration tests, passing a fixed seed (where the provider supports it) together with temperature 0 gets you much closer to deterministic without going fully mocked. It's not a guarantee - the same seed can still drift across model versions - but it noticeably reduces the retry count. Pairs well with the normalization point in the comments.
The client-level fixture can also distinguish a truncated response from malformed JSON. Add a response with finish_reason set to length, then check that extraction stops before saving a partial result and that any retry is bounded. A valid-looking prefix should not cross the same persistence boundary as a complete response.
The invoice example seems to call for normalized value comparison rather than removing the value assertion. '1,234.56' and '1234.56' can represent the same amount, but a schema-valid positive number could still be the wrong total, and the real-API test shown would accept it.
I would parse the amount under an explicit locale and currency contract, then compare it with the labelled invoice amount. Keep the client mocks for malformed output and application behavior, and keep a separate extraction-quality suite with known source documents. That preserves the useful deterministic test boundary while ensuring that eliminating formatting flakiness does not also eliminate the test's ability to detect an incorrect extraction.
The schema-versus-value distinction is useful, but there’s an interesting gap between “valid output” and “correct output.” A response can satisfy the schema perfectly while extracting the wrong invoice total, misclassifying a document, or producing a summary that misses a critical fact. I’d treat those as separate testing layers: deterministic tests verify the contract and how the application handles the response, while a smaller set of evaluation cases checks semantic correctness against known expectations. That also makes flaky tests easier to avoid without weakening coverage. The goal isn't to make every assertion deterministic; it's to make each test responsible for a property that can actually be judged reliably.
This is a really practical take on something that can get confusing very quickly with AI-powered applications.
The part about flaky tests being an “assertion strategy problem” really stood out to me. It’s tempting to test an LLM response the same way we would test a traditional function, but expecting the exact same wording from a model is setting the test up to fail.
I also liked the distinction between testing our application logic with mocks and testing the actual model behaviour separately. That feels like a much healthier approach than having the entire test suite depend on an external API being available and returning exactly what we expect.
The idea of asserting on structure and behaviour rather than exact generated text is probably something a lot of teams will learn the hard way. Great article, and very useful for anyone trying to move LLM features from a demo into production.
Rishita Sharma
Sharing practical lessons from building Naraway and shipping useful products.
The invoice total example is exactly how my CI got ignored too. One formatting change from the model and suddenly everyone just reruns until green. Switching to schema checks plus a small normalizer for numbers and dates fixed most of it for me. The part I still struggle with is free text fields. Do you assert anything on those beyond length and required keywords?