This book offers 159 practical recommendations, each spanning about a page, aimed at helping you transform loose and helpless unit tests into ruthless and furious ones.
Yegor is a software developer in Huawei; a CTO at Zerocracy, an AI-based software platform for management; a VC at SeedRamp.com; a regular blogger at www.yegor256.com; a co-author of EOLANG, an experimental object-oriented programming language; a proud holder of PMP and OCMEA certifications; a hands-on Java developer and a lead architect of Rultor.com and Takes.org. Yegor lives in Moscow, Russia.
I read Angry Tests by Yegor Bugayenko, and some of its recommendations on unit testing have already made their way into my AGENTS.md. However, I found the chapter on fuzzing too brief. In my view, it leaves readers without enough guidance on how to organize fuzz testing or evaluate whether it is working effectively.
Fuzzing is often perceived as expensive. In my experience, modern AI models have significantly reduced the effort required to build harnesses and specialized tooling. Today, you can quite literally “vibe-code” a prototype for your language and a specific task, such as testing how state accumulates in the Erlang VM. You can use libFuzzer or AFL++ as the engine, or build a custom engine if necessary. Of course, the generated tooling still needs to be validated.
I would have liked the book to explain a few practical questions: why a fuzzer needs coverage feedback, how to validate a harness, and when to stop a long-running campaign.
In coverage-guided fuzzing, coverage helps the engine select promising inputs for further mutation. If an input reaches a previously unseen part of the program, it is worth preserving and exploring further. The quality of this feedback directly affects the effectiveness of the search: noise in the coverage signal can look like progress.
My approach is to run full fuzzing campaigns in separate pipelines, then reuse the accumulated input corpus in short CI runs lasting around 15 minutes. Regular CI should not have to wait days for an exploratory campaign to finish. Testing should also avoid disrupting the development process.
But “we fuzzed it for 15 minutes” only defines the CI budget. A long-running campaign needs a different stopping criterion.
I track coverage growth. If a properly functioning fuzzer produces no new reproducible coverage for N hours, I stop the current campaign and review its configuration: the harness, seed corpus, mutations, and input constraints. The value of N depends on the target and its execution speed.
Stopping under this rule means that the current strategy has stopped producing measurable progress. It does not establish that the target is free of defects: new bugs can still be found along paths that have already been covered. It is, however, a useful signal that the campaign needs an engineer’s attention.
When selecting targets and developing harnesses, I use a Code Property Graph to investigate data flows and identify parts of the program worth fuzzing. It is then essential to verify that the input actually reaches the intended logic, and that the harness itself is free of defects and can withstand prolonged execution.
Reproducibility is particularly important. Given the same input, initial state, and environment, the goal should be to obtain the same coverage signal. Otherwise, the fuzzer may retain inputs because of incidental differences between executions.
For stateful fuzzing, this requires carefully defining the boundaries of a test case. A single input can describe an entire sequence of operations, with state deliberately accumulating throughout that sequence. Between independent sequences, the initial conditions should be restored so that one test does not unintentionally influence the next.
The harness therefore needs to control randomness, time, background threads, and the external environment, while releasing resources and cleaning up temporary files. Random numbers are not inherently a problem if their use is reproducible and determined by the input.
VMs and JIT compilation make this more complicated. Warm-up, changes in compiler state, and instrumentation details can alter the observed coverage independently of whether a mutation is useful. This needs to be checked explicitly. The presence of a JIT does not automatically mean that the fuzzer will observe false coverage novelty.
AFL++ provides a stability metric for monitoring this. It reflects how consistently the observed coverage map behaves across repeated executions. My watchdog monitors both the absence of coverage growth and stability. If stability drops below 90%, I interrupt the campaign and investigate. This is my operational threshold; an acceptable level may differ between targets. In my experience, these problems occur more often with VMs and operating system components.
libFuzzer has no direct built-in equivalent of this metric. One option is to add a separate calibration mode: execute a fixed input repeatedly, collect the coverage features from each execution, and compare the resulting sets.
A useful companion reading is [Chris Wellons’s article on practical fuzzing with AFL++](https://nullprogram.com/blog/2025/02/05/), particularly the section on isolating tests in persistent mode.
I would have liked the book to provide more of these practical foundations: how to obtain trustworthy feedback, validate a harness, connect long-running campaigns with CI, and recognize when the search strategy needs to be revisited.
The book is great. Full of practical advice and examples, useful for both junior and seasoned developers. One idea that really stuck with me was the Fast vs. Deep test classification — a very insightful distinction. Highly recommend reading it!