A test can be green and still lie. It passes, the CI shows its tidy little ✅, and yet on every run it dumps a cascade of warnings and errors into the console. We’ve grown used to it: that noise is part of the scenery, we scroll past it without seeing it anymore. That’s exactly where the problem starts.
My stance is simple: a test that succeeds (🟢) but pollutes the console (stdout or stderr) should be treated as a test that fails (🔴). Not out of purism, but because that pollution is almost always the visible symptom of a very real problem.
Why a warning shouldn’t go unanswered
When a test spits noise into the console, it’s tempting to shrug it off: “it passes, so everything’s fine.” But that noise has causes, and it has costs.
- A warning is rarely free. More often than not it signals something justified: a misused API, a dependency-injection slip that boots a real SDK where a test double was expected, a resource left unclosed, and so on. The message is there to warn you: ignoring it is like silencing the alarm without checking where the smoke is coming from.
- Noise adds to the cognitive load. Warnings drown out the useful information you can, and should, exploit in the test output. This is especially true in the frontend ecosystem, where tools like React Testing Library tend to be very verbose. When a test turns red, you want to understand immediately why; not have to dig through a hundred lines of stray logs to isolate the one that matters.
- Writing to stderr can break the build. Some tools interpret any write to stderr as an error. The consequence: the test task fails even though, technically, every test passed. The signal becomes inconsistent, and trust in the CI erodes.
- Too much I/O is expensive. Logging heavily isn’t neutral: input/output slows tests down, all the more so when you run them thousands of times a day locally and in continuous integration.
None of these reasons is dramatic on its own. It’s precisely their silent accumulation that’s the problem, and that’s where a theory from another field comes in.
The trap of the Broken Windows Effect
The Broken Windows Effect comes from a theory formulated by George L. Kelling, a criminology professor, and James Q. Wilson, a political science professor. Their observation: if a window in a building is broken and left unrepaired, all the other windows will soon end up broken too.
The mechanism is psychological. A degradation left in place sends a signal: nobody is watching here, nobody cares. From then on, the next degradation costs less morally, and the decline accelerates on its own.
The analogy with tests is direct. The first tolerated warning is the first broken window. As long as it stands alone, you might think it’s harmless. But it sets a new norm: since we accept that noise, why not the next? One thing leads to another, the console becomes unreadable, no one can tell signal from noise anymore, and you lose the ability to react when a real alert shows up.
The principle doesn’t stop at the test suite. It applies far more broadly, across an entire codebase: a // TODO never dealt with, a tolerated any, a lint warning disabled “temporarily”… every broken window you leave in place makes the next one easier to accept. Making sure these warnings never start to settle in is maintaining the building before it falls into disrepair.
Two ways to respond to the noise
Faced with a test that pollutes the console, you have two approaches, and they are not equal.
Approach 1: silence the symptom
The first consists of hiding the logs. You can monkey patch console.log and console.error before a test to neutralize them, or rely on the runners’ native options:
jest --silent
vitest --silent
It’s fast, and the verbosity disappears. But here you’re only treating the symptom. The warning still exists; you’ve simply decided not to see it anymore. And by hiding it, you deprive yourself of useful information, tied to something that may eventually cause real problems in production. It’s putting tape over the warning light on the dashboard.
Approach 2: hunt for the root cause
The second approach is the one I favor, in a Lean spirit: trace things back to the root causes and understand the why of the warning. Most of the time, it’s there for a good reason. Rather than ignoring it, you simply set out to fix the underlying problem: the misused API, the badly injected dependency, the resource left open. Once the cause is handled, the noise disappears on its own, and for good.
To make this discipline impossible to bypass, you can go further and force tests to fail as soon as anything is written to stdout or stderr. The vitest-fail-on-console package integrates with Vitest to do exactly that:
import failOnConsole from "vitest-fail-on-console";
failOnConsole({
shouldFailOnerror: true,
shouldFailOnWarn: true,
shouldFailOnLog: false,
shouldFailOnInfo: false,
shouldFailOnDebug: false,
});
From then on, a test that emits a warning or an error in the console turns the suite red: the broken window triggers the alarm immediately, instead of being tolerated in silence.
This principle is neither isolated nor extreme: it’s applied all over the devtools ecosystem. Rush.js, for instance, fails a project’s build by default as soon as a warning is emitted. The logic is always the same: don’t let the noise accumulate until it becomes the norm.
Conclusion
A green test that pollutes the console is not a clean test: it’s a test that succeeds despite a problem it’s pointing right at you. The Broken Windows Effect reminds us that no degradation is ever truly isolated: the first broken window you tolerate opens the way for all the ones that follow.
So the right response isn’t to silence the noise, but to eliminate its cause. And to hold that discipline over time, the safest bet is to automate it: failing the suite as soon as a warning appears turns a good intention into a guardrail. A silent console isn’t a cosmetic detail: it’s proof, on every run, that nothing is quietly degrading behind the scenes.