Skip to content
Preptima
mediumConceptEntryMidSenior

A test fails in CI and all you get is 'expected true, got false'. What should the framework have given you instead?

A failure has to be diagnosable from its artefacts alone, because nobody can attach a debugger to a CI run: that means assertions that report the compared values, a message naming the business expectation, and captured evidence of the state at the moment of failure.

4 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate know that a boolean assertion discards the information needed to diagnose the failure
  • Whether the answer distinguishes what the assertion library reports from what the test author must supply
  • That reproducing locally is treated as a fallback rather than the primary diagnostic route
  • Whether artefact capture is specified per failure rather than as blanket logging
  • Does the candidate connect diagnosability to how quickly a red build gets triaged or ignored

Answer

Why the boolean assertion is the problem

assertTrue(user.isActive()) and expect(isVisible).toBe(true) throw away the only thing that would have helped. The comparison happened inside your own expression, so by the time the assertion runs it has one bit left, and one bit cannot be printed usefully. Every assertion library will report both sides of a comparison it performed itself, which is why expect(user.status).toBe("active") fails with expected "active", received "suspended" and tells you the answer immediately.

So the first move is mechanical: stop pre-computing booleans and hand the assertion library the actual values to compare. assertEquals over assertTrue, matchers over toBe(true), collection matchers rather than asserting on list.contains(x). This single habit accounts for a large share of undiagnosable failures, and it costs nothing.

When a boolean genuinely is the value under test — a feature flag, a permission check — you supply the message yourself, stating the expectation in the domain's language rather than restating the code. "Expected checkout to be enabled for a user in the EU beta cohort" is diagnosable. "isEnabled was false" is the same one bit with more characters.

The failure output is read by somebody with no context

The design constraint that people miss is who reads this. It is not you, five seconds after writing the test. It is whoever is on rota when the build goes red at 09:00, possibly on a branch they did not write, and their decision is a triage one: is this my change, an environment problem, or a flake? They will make that call from the failure message and the attached artefacts, because opening the test source and reasoning about it costs ten minutes and reruning the pipeline costs one click.

That asymmetry is what makes diagnosability an engineering property rather than a nicety. A suite whose failures are cheap to interpret gets fixed. A suite whose failures require investigation gets rerun, then retried automatically, then ignored, and the tests are still running but no longer function as a gate.

What a diagnosable failure contains

Four things, and they come from different places.

The compared values come from the assertion library, for free, provided you let it do the comparing. The expectation in domain terms comes from you, as the assertion message, and it should say what should have been true and for which case. The state at the moment of failure comes from the framework's capture hooks: for a browser test that means the screenshot, the DOM snapshot and the console log; for an API test the full request and the full response body including headers; for a database-backed test the identifiers of the rows the test created, so somebody can go and look. And the identity of the run comes from the harness — which commit, which worker, which browser and version, which environment, and a correlation identifier that ties the test to the server-side logs it generated.

That last one is the most undervalued. If each test sends a header carrying its own name and run identifier, and the service logs that header, then a failed assertion in CI leads directly to the server's own account of what it did. Without it, you have a client-side symptom and a log search over a time window shared with every other test on every other worker.

Timeouts need a different treatment from assertions

Most automated-test failures are not failed comparisons, they are waits that expired, and the default message for those is usually worse. "Timed out after 30000ms" names the duration and nothing else. What you need is the condition being waited for, the last value observed, and how many times it was polled — because "waited for the order status to become CONFIRMED, last saw PENDING after 30 polls" and "waited for the order status to become CONFIRMED, last saw null after 30 polls" point at two entirely different bugs. The first is a slow or stuck workflow; the second is that you are looking in the wrong place, or at a record that was never created.

Any polling helper you write should therefore keep the last observed value and the last thrown error, and put both in the message when it gives up. A helper that swallows the intermediate exceptions and reports only its own timeout has destroyed the evidence, and this is one of the most common defects in in-house wait utilities.

Artefacts are worthless if nobody can find them

Capture on failure only. Traces and video for every passing test inflate run time and storage and bury the interesting artefact among thousands of dull ones. Attach artefacts to the failing test in the report rather than dropping them in a bucket, name them after the test and the attempt, and retain them long enough to survive a weekend.

Retries deserve a specific decision here. If a test is retried and passes, keep the artefacts from the failed attempt, because that attempt is the only record of an intermittent fault you will ever get. A retry policy that discards the first attempt's evidence converts real bugs into invisible ones, which is the mechanism by which a genuine race condition lives in a codebase for a year.

Assume nobody will ever attach a debugger to a CI failure. Everything needed to distinguish a product bug from a bad test from a broken environment has to be in the message and the artefacts, or the failure will be rerun instead of read.

Likely follow-ups

  • Your assertion now prints both values and they look identical on screen. What next?
  • How would you make a timeout failure say what it was waiting for and what it saw instead?
  • What do you capture on a failure that you deliberately do not capture on a pass, and why?
  • A single failing test produces 200 MB of artefacts. How do you decide what to keep?

Related questions

automationassertionsdebuggingtest-reportingci