Improve software quality by classifying escapes

A bug count that climbs after every release is a filing rate before it is a quality rate. How to improve software quality: classify the escapes, then spend.

Four clear glass roots rise from a slab, with the longest origin glowing orange

A bug count that climbs after every release is measuring the filing rate before it measures the product, and the next quarter’s budget is about to be spent on a guess. More automated tests, stricter review and a QA hire each prevent one origin of defect. Until last quarter’s escapes are tagged with their origin, nobody knows which one.

A climbing count is a filing rate first

Several things push a bug count up without the code getting worse. More users reach more corners of the product, so defects that were always there get found. A new reporting channel, a widget on the page or a portal for clients, makes filing easy enough that people file what they used to tolerate. The same defect arrives under three titles from three reporters because none of the reports carried enough to match them. And a backlog that is never closed makes the count cumulative, so it climbs by construction.

The first job is to separate three numbers that the tracker shows as one. Open tickets is a backlog measure. Tickets filed is a channel measure. Unique defects that reached production per release, each an escaped defect, is the only one that says something about the release. A count of tickets that rises after a feedback widget goes live measures the widget; a count of unique escapes per release that rises is the product getting worse, and only the second one deserves a quarter’s budget.

Every prevention spend fixes one origin

The candidate spends on an engineering manager’s list are each a fix for one kind of defect, and a defect has an origin whether or not anybody recorded it.

OriginWhat the ticket looks likeWhat prevents it
Requirement misreadWorks as built, wrong as intended; the product owner says that is not what was meantAcceptance criteria written as examples before coding, and a second reading by somebody who did not build it
Logic error in new codeReproduces on a clean path through the new featureUnit tests on the new code, and review
RegressionUsed to work, broke in a change nobody thought was relatedA regression pass chosen by risk, and characterization tests on the code being changed
IntegrationWorks in isolation, fails at the boundary with another service or a third partyContract tests on the boundary
Environment or compatibilityWorks in one browser, device or configuration and not anotherA compatibility matrix named in the definition of done
Data or stateWorks with test data, fails on the shape production holdsTest data that mirrors production shapes, and boundary values in the cases

Read the manager’s list against the right-hand column. Test automation, in the form of a regression suite, funds the regression row. Stricter review funds the logic row. A QA hire funds the requirement row and the exploratory pass, and reaches the environment row only if the compatibility matrix is written down for them. None of the three touches the integration row, and a quarter spent on the regression row while the escapes are coming from a third-party boundary leaves the count exactly where it was, with a larger test suite to maintain.

Classify last quarter’s escapes before spending

The method is a tag and a table. Take every defect that reached production over the last few releases. For each, a person who knows the product records two things: the origin row it belongs to, and the check that should have caught it. That second tag is what turns a list of bugs into a list of gaps, because a defect that should have been caught by review and was not is a fact about review, and a defect no existing check would ever have caught is a fact about the checks.

Then read the distribution. The largest bucket is where the quarter goes. The number to watch as the spend lands is defect removal efficiency, the share of a release’s defects caught before it shipped, because it rises when more of a release’s defects are caught before shipping, and a channel that makes production reports easier to file pushes it down rather than up, so the rise that started the panic cannot flatter it.

The tagging is triage work, done at the moment a ticket is read, and it is the reason to insist that triage happens at all. Retrofitting it onto a year of closed tickets produces guesses dressed as data, since the person doing it no longer remembers the defect and the evidence that would have told them is gone.

Two origins need tests most teams have not written

Write more tests is the advice everyone gives, and it fails on two of the rows because the tests a team already knows how to write do not reach them.

The regression row is where the code nobody understands and everybody is afraid to touch shows up. Unit tests on new code do nothing for it. What does is a characterization test: a test written against the code as it is, recording what it does today, correct or not, so that a change shows what it altered. Nothing about the test asserts the behaviour is right. It asserts the behaviour is the same, which is the only question a regression is about.

The integration row is about boundaries, and the tests a team already has reach neither side of one. A unit test mocks the other side and proves nothing about it; an end-to-end test against a live copy of the other side is slow and fails for reasons that have nothing to do with the change. A contract test sits between them: each side tests against the agreed shape of the exchange, so a change on either side that breaks the agreement fails a test on the side that broke it. With a third party that will not run its half, the provider side of the check is a scheduled verification of the recorded contract against the live service, which catches the break after it happens rather than before. When the classification puts integration at the top, this is the spend.

The unknown bucket decides the first spend

Classification has a precondition, and it is the limit the whole plan rests on. A ticket can only be tagged with an origin if it carries enough to tell one origin from another. A console exception points at logic. A failed request with its status points at an integration. A report that reproduces on one browser and no other points at compatibility. A ticket that says the page broke points at nothing, and it goes into a bucket labelled unknown.

When unknown is the largest bucket, the first spend goes to how bugs are reported rather than to any prevention. A report filed through evidence capture carries the console log, the network requests with their statuses, and the browser, device and page path of the session that produced it, which is the same set of facts the origin table sorts on, so a triager can assign the row in the time it takes to read the ticket. Until the reports carry that, the origin table has a hole in it, and a hole in the table is a guess in the budget.

The tag cannot be back-filled for tickets whose evidence has gone, so the first quarter of classification measures only the tickets filed after tagging began, and the spending decision it supports is a quarter out. The distribution also moves once the largest bucket is addressed, so each decision is good for one quarter, after which the table is read again and the answer may be different.

Common questions

How do you improve software quality when bug counts keep rising after each release?

Separate the count first: open tickets, tickets filed and unique escapes per release are three different numbers, and only the third measures the release. Then tag each escape with the origin that produced it and the check that should have caught it. The largest origin decides where the next quarter's spend goes.

Why does the bug count go up after adding a feedback widget?

The count goes up after a feedback widget because filing got easier, and defects that users were living with silently start arriving as reports. A rise that follows a new reporting channel measures the channel, and the number to watch instead is escapes per release, counted as unique defects rather than as tickets.

Should we spend on test automation or on code review to reduce bugs?

Neither automation nor review deserves the money until the escapes are classified. Automation prevents regressions; review catches logic errors in new code; a requirement misread is caught by neither and needs criteria written as examples plus a second reading. Spending on the origin that produces the fewest escapes leaves the count where it was.

What is a characterization test and when is it worth writing?

A characterization test pins what a piece of code does today, correct or not, before anyone changes it, so the change shows what it altered. Worth writing on code nobody on the team understands and everybody is afraid to touch, which is where regressions from unrelated changes come from.

How do you find the root cause of recurring bugs?

The root cause of recurring bugs is found at triage, by a person who knows the product, one ticket at a time, using the evidence on the ticket: a console exception points at logic, a failed request with a status points at an integration, a report that reproduces in one browser only points at compatibility. A ticket carrying none of that goes in an unknown bucket.

The Notarus team

The people who build Notarus. We write up the decisions behind the product when the reasoning is worth more than the changelog line.

Keep reading

Reports with the evidence attached.

Start free, one script tag.

<script src="…/widget.js" data-key="pb_pub_…">