Skip to main content
Preslav Rachev Preslav Rachev
  1. My Writings /Going with Go /

Using Scoped Coverage to Prune an AI-Generated Go Test Suite

·6 mins
AI coding agents are very good at adding tests. Getting them to understand and simplify the tests already there is a different story.

I am currently auditing a large Go project that has been put together with the heavy use of AI coding tools like Claude Code and Codex. When I opened it for the first time, it boasted several hundred tests. Yet, average test coverage across the project sits somewhere in the low 40 percent range.

That combination made me suspicious.

AI agents seem much happier adding code on top of code than stopping to make sense of what is already there. The same applies to tests. When asked to fix a bug or implement a feature, an agent will gladly add another test. It rarely steps back and asks whether an existing test could express the same behavior with one more table entry.

My suspicion was that many of those hundreds of tests were exercising the same code. Perhaps they were checking different assertions. Perhaps some were genuine duplicates. Either way, substantial overlap would point me toward parts of the suite worth reading and simplifying.

Aggregate coverage could not answer that question. It told me how much code the entire suite touched, while I needed to know which test touched which code.

Scoped Coverage with Tobari #

In my search for estimating the actual contribution of each test to the final coverage, I ended up finding a tool called Tobari. There may be others like it, but this one was the first that did what I wanted, so I stuck with it.

What I call Contribution, Tobari calls Scoped Coverage. Instead of producing one coverage number for the whole package, it records a mapping between individual tests and the statements they execute.

I installed it and started running it against the project one package at a time:

go install github.com/goccy/tobari/cmd/tobari@latest

GOFLAGS="$(tobari flags)" go test ./path/to/package

The test run creates tobari/tobari.json, which can then be turned into an interactive HTML report:

tobari html tobari/tobari.json

The report contains an overlap matrix. Clusters quickly become visible, and a separate ranking gives me concrete pairs of tests with their match rate and number of common statements.

The overview would also give you a list of each test-pair overlap:

Granted, this specific package did not have that many tests, but there were others, far worse, where almost everything was red.

Having this information now, I can hand back to an AI agent. I give it a highly overlapping pair and ask it to inspect both tests. If they express variations of the same behavior, it can consolidate them into a higher-level table-driven test. If one is a genuine duplicate, it can remove it. Or, you know, in good old-school fashion, I could use my own programming skills and do it myself.

In one of the first packages I audited, Tobari identified several tests with overlaps around 70 to 80 percent. The agent was able to consolidate one such group into a table-driven test.

Of course, test coverage remained exactly the same, and that was perfectly fine. I had one strict requirement: coverage after the change had to remain at least exactly where it was before. Any increase was a bonus. The actual goal was to prune the test code drastically enough that I could see the forest for the trees again.

Coverage Points Me Where to Look #

IMPORTANT: A high overlap score does not prove that two tests are duplicates. They may execute the same lines while making different assertions about the result. Equal coverage after consolidation does not prove that every meaningful check survived either.

This is where my eyes and reasoning need to step in. The report narrows down a suite of hundreds of tests to a handful of concrete places worth examining, but it’s up to me to say if I am OK with the changes or not. While the agent handles much of the mechanical consolidation, I get an opportunity to look selectively at facets of the code and use my brain.

That last part matters to me. An audit should help me understand the system in the first place. If I delegate the entire process, I may end up with fewer lines while remaining equally confused about what they mean.

Side Note: What Does tobari flags Actually Do? #

Commands hidden behind shell substitution always make me a little uneasy. This one looks harmless:

GOFLAGS="$(tobari flags)" go test ./path/to/package

But the thing is, it changes how the entire test binary gets built. So, what is happening behind the scenes?

At the time of writing, tobari flags expands to the equivalent of:

-cover -toolexec=/path/to/tobari

The -cover flag asks the Go toolchain to compile the selected package with coverage counters. The -toolexec flag places Tobari in front of Go’s build tools, including the compiler, coverage tool, and linker.

During the build, Tobari creates modified inputs inside Go’s temporary $WORK directory. It adds hooks to the runtime and testing machinery, then compiles those temporary inputs into the test binary. The original .go files in the repository remain untouched.

Compile-time instrumentation is an existing technique in Go, albeit not something I’ve seen done a lot. Certainly not as much as on the JVM - there, annotation processing and all kinds of bytecode magic are really commonplace. It’s cool, though, that Tobari applies it to a particularly useful question: which parts of the program did this specific test exercise?

Found this post useful? Share on HN

Deterministic Tools Make Better Agent Boundaries #

The more I use coding agents, the more I appreciate deterministic tools around them.

An AI agent may come up with a good consolidation. It may also remove an assertion because it looks redundant, invent an abstraction I never asked for, or confidently declare two tests equivalent when they are not. Asking it to “clean up the tests” leaves far too much room for interpretation.

The creative part remains nondeterministic, but the acceptance criteria come from tools that produce repeatable results. I use the same pattern with linting. I have accumulated a long list of lint rules that I ask my agents to exercise every now and then. Those rules keep the code within dimensions I can still understand.

This symbiosis is one of the reasons I am still somewhat OK with AI-assisted programming. Agents are useful when deterministic tools constrain their appetite for adding more. Linters, compilers, tests, and Scoped Coverage give them rails. But ultimately, your judgment decides where the train should go.

I am still working through this project package by package. I do not yet know how much test code will disappear by the end. For now, fewer tests with the same coverage is enough.

Have something to say? Join the discussion below 👇

or reply via

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting