---
title: 'Quality Metrics'
source: 'https://academia.sh/en/courses/testing-process/quality-metrics'
course: 'The Testing Process and Automation Infrastructure'
language: en
updated: '2026-08-23T14:25:19+00:00'
license: 'CC BY-SA 4.0'
---

# Quality Metrics

Computing escaped defects, rework, and cycle time from the same event set: measuring a single pipeline-cheapening decision across two periods, scanning six metrics' thresholds together with their sources, counting which metric changes a decision, and the attention cost of a metric that changes none.

The previous lesson made a single run's result readable and resolved that run's root into a
decision. One question a report cannot answer remained: is this team testing better over time.
Schema mismatch showing up one night is an event; showing up every week is a process defect,
and a single run's report makes the two look the same. This lesson moves the measure to the
period level.

What is measured here is not the system, it is the team. The running system's response time,
error rate, and saturation are the Performance Anti-Patterns and Monitoring course's subject;
the metrics here measure the testing itself. The resource distributed is again **attention**,
and its quantity is the metric count: every metric wants to be collected and read once per
period, and in exchange is expected to produce a decision. If it does not, it is noise.

## Three Metrics, One Data Set

**Defect leakage rate** is the ratio of defects that reach production without being caught at
any pipeline stage to total defects; the escaped defects in its numerator are the same concept
as M21/K03's escaped real defects. **Rework** is work redone because of a defect, and it is
measured in hours; this is a different concept from the **failback** the Resilience and
Reliability course describes — the return to the primary copy after a takeover. **Cycle time**
is the time from a change being submitted to it entering the main branch; the time up to
release is **lead time**, and this lesson does not measure it.

What the three share is this: all of them come out of a single event set — which defect was
caught at which stage. Stage order, feedback minutes, and the defect set are taken from the
Testing in Continuous Integration lesson's three-frequency arrangement.

```js
// period.mjs -- five metrics for two periods over the same defect set
// TP9 (automation-infrastructure/01): 100 changes, 20 workdays. Class names and per-hundred-
// change counts are from the same lesson's TP7 defect set; stage is the place of the team
// catching that class in the three-frequency arrangement. Total 80 defects, 20 clean changes.
export const CLASSES = [
  ['boundary comparison', 13, 'fast'], ['breaking change', 6, 'fast'],
  ['request shape drift', 5, 'fast'], ['field lost from contract', 4, 'fast'],
  ['schema mismatch', 8, 'mid'], ['card status not updating', 7, 'mid'],
  ['status code mapping', 6, 'mid'], ['missing warning on rejected request', 4, 'mid'],
  ['data loss in migration', 3, 'mid'], ['check-then-act race', 2, 'mid'],
  ['distribution-dependent report defect', 2, 'mid'], ['latency regression', 4, 'nightly'],
  ['concatenated query', 3, 'nightly'], ['recovery gap', 1, 'nightly'],
  ['capacity limit', 1, 'nightly'], ['accessibility criterion', 5, 'release'],
  ['percentile read from few samples', 1, 'release'], ['semantic mismatch', 3, 'escapes'],
  ['process-lifetime state', 2, 'escapes'],
];

// Feedback minutes read from automation-infrastructure/01's three-frequency arrangement.
// TP20: rework hours by stage -- a late-caught defect grows from lost context, retesting, and
// incident handling in production. TP21: an escaped defect is reported in production after one
// release cycle (2400 min); a change's baseline cycle time is 480 min.
export const FEEDBACK = { fast: 8, mid: 49, nightly: 425, release: 2221, escapes: 2400 };
export const REWORK = { fast: 0.25, mid: 1, nightly: 3, release: 6, escapes: 16 };
export const BASELINE = 480, DAY = 480, CHANGES = 100;
export const CAPACITY = 800;   // TP22: 5 people x 20 workdays x 8 hours

// Period 2's only difference is automation-infrastructure/01's "manual off pipeline"
// arrangement: the manual security review leaves the release gate, and accessibility criterion is no longer caught.
export const period = (second) => CLASSES.map(([name, n, a]) =>
  [name, n, second && name === 'accessibility criterion' ? 'escapes' : a]);

export function measure(k) {
  const defectCount = k.reduce((a, [, n]) => a + n, 0);
  const missed = k.filter(([, , a]) => a === 'escapes').reduce((a, [, n]) => a + n, 0);
  const rework = k.reduce((a, [, n, x]) => a + n * REWORK[x], 0);
  const feedback = k.reduce((a, [, n, x]) => a + n * FEEDBACK[x], 0) / defectCount;
  // Cycle time: clean and escaped changes converge at the baseline; a caught defect waits on
  // its stage's feedback and its rework.
  const d = [];
  for (let i = 0; i < CHANGES - defectCount; i += 1) d.push(BASELINE);
  for (const [, n, x] of k)
    for (let i = 0; i < n; i += 1)
      d.push(x === 'escapes' ? BASELINE : BASELINE + FEEDBACK[x] + REWORK[x] * 60);
  d.sort((a, b) => a - b);
  const percentile = (q) => d[Math.ceil((q / 100) * d.length) - 1] / DAY;
  return { defectCount, missed, leak: missed / defectCount, rework, reworkShare: rework / CAPACITY, feedback,
    median: percentile(50), p90: percentile(90), caught: defectCount - missed };
}
```

Cycle time's calculation turns on one detail that decides the whole outcome: **an escaped
defect does not lengthen cycle time.** No one sees it, so the change merges on time; its cost
is paid in production, in a different column. This line is why the table further down shows
cycle time moving in the wrong direction.

## Two Periods, One Decision

There is exactly one difference between the two periods compared. The first period uses the
three-frequency arrangement. In the second period, the team decides to cheapen the pipeline and
removes the manual security review from the release gate — the Testing in Continuous
Integration lesson measured that this arrangement drops from 98.2 to 86.2 minutes per change.
The same lesson also measured that this decision raises escaped defects from five to ten; the
question here is whether the metrics can see that.

```js
// decision.mjs -- five metrics' value in two periods, and which one changed a decision
import { CLASSES, REWORK, CAPACITY, CHANGES, period, measure } from './period.mjs';

const PERIODS = [measure(period(false)), measure(period(true))];
const p = (x, n) => String(x).padStart(n);
const b = (x, n) => x.toFixed(n);

// Threshold and its source. A metric with no threshold has no decision tied to it either.
const METRICS = [
  { name: 'leakage rate', get: (x) => x.leak * 100, format: (v) => `${b(v, 2)}%`, threshold: 8,
    decision: 'add a test to inventory' },
  { name: 'rework share', get: (x) => x.reworkShare * 100, format: (v) => `${b(v, 2)}%`, threshold: 25,
    decision: 'move the class to an earlier stage' },
  { name: 'cycle time p90', get: (x) => x.p90, format: (v) => `${b(v, 2)} days`, threshold: 3.5,
    decision: 'change stage placement' },
  { name: 'cycle time median', get: (x) => x.median, format: (v) => `${b(v, 2)} days`, threshold: null,
    decision: 'none' },
  { name: 'avg feedback', get: (x) => x.feedback, format: (v) => `${b(v, 1)} min`, threshold: 480,
    decision: 'change gate frequency' },
  { name: 'caught defects', get: (x) => x.caught, format: (v) => String(v), threshold: null,
    decision: 'none' },
];

console.log(`${CHANGES} changes, ${CLASSES.length} defect classes, ${PERIODS[0].defectCount} defects, `
  + `${CHANGES - PERIODS[0].defectCount} clean changes; team capacity ${CAPACITY} hours`);
console.log(`\n${'metric'.padEnd(20)}${p('period 1', 11)}${p('period 2', 11)}${p('threshold', 11)}`
  + `${p('P1 over', 9)}${p('P2 over', 9)}${p('changed', 9)}${'  linked decision'}`);
let changing = 0;
for (const g of METRICS) {
  const [v1, v2] = [g.get(PERIODS[0]), g.get(PERIODS[1])];
  const [c1, c2] = [g.threshold !== null && v1 >= g.threshold, g.threshold !== null && v2 >= g.threshold];
  const changed = g.threshold !== null && c1 !== c2;
  if (changed) changing += 1;
  console.log(`${g.name.padEnd(20)}${p(g.format(v1), 11)}${p(g.format(v2), 11)}`
    + `${p(g.threshold === null ? '-' : g.format(g.threshold), 11)}${p(g.threshold === null ? '-' : (c1 ? 'yes' : 'no'), 9)}`
    + `${p(g.threshold === null ? '-' : (c2 ? 'yes' : 'no'), 9)}${p(changed ? 'YES' : 'no', 9)}`
    + `  ${g.decision}`);
}
// TP23: a metric's per-period collection and review cost is 30 minutes.
console.log(`\n${METRICS.length} metrics x 30 min = ${METRICS.length * 30} min/period review; `
  + `changing a decision ${changing}, noise ${(METRICS.length - changing) * 30} min`);

// The decision also asks "which class": rework hours broken down by class.
console.log(`\ntop three classes eating rework hours `
  + `(total ${b(PERIODS[0].rework, 0)} -> ${b(PERIODS[1].rework, 0)} hours)`);
console.log(`${' '.repeat(3)}${p('period 1', 32)}${p('period 2', 32)}`);
const rank = (second) => period(second).map(([name, n, a]) => [name, n * REWORK[a]])
  .sort((x, z) => z[1] - x[1]).slice(0, 3);
const [r1, r2] = [rank(false), rank(true)];
for (let i = 0; i < 3; i += 1)
  console.log(`${`${i + 1}.`.padEnd(3)}${p(`${r1[i][0]} ${b(r1[i][1], 0)} hrs`, 32)}`
    + `${p(`${r2[i][0]} ${b(r2[i][1], 0)} hrs`, 32)}`);
```

```
100 changes, 19 defect classes, 80 defects, 20 clean changes; team capacity 800 hours

metric                 period 1   period 2  threshold  P1 over  P2 over  changed  linked decision
leakage rate              6.25%     12.50%      8.00%       no      yes      YES  add a test to inventory
rework share             22.75%     29.00%     25.00%       no      yes      YES  move the class to an earlier stage
cycle time p90        2.26 days  1.23 days  3.50 days       no       no       no  change stage placement
cycle time median     1.05 days  1.05 days          -        -        -       no  none
avg feedback          386.8 min  398.0 min  480.0 min       no       no       no  change gate frequency
caught defects               75         70          -        -        -       no  none

6 metrics x 30 min = 180 min/period review; changing a decision 2, noise 120 min

top three classes eating rework hours (total 182 -> 232 hours)
                           period 1                        period 2
1.         semantic mismatch 48 hrs  accessibility criterion 80 hrs
2.    process-lifetime state 32 hrs        semantic mismatch 48 hrs
3.   accessibility criterion 30 hrs   process-lifetime state 32 hrs
```

## Thresholds and Their Sources

A metric's value alone does not carry a decision; what turns it into one is a threshold, and a
threshold has to have a source.

- **Leakage rate, 8 percent.** Its source is the inventory's blind spot: the Testing in
  Continuous Integration lesson counted two defect classes that no test team sees, five of
  eighty defects. That is leakage rate's floor — 6.25 percent. The threshold cannot be set
  below that floor, because an unreachable threshold produces no decision in any period, it
  only leaves a permanent alarm.
- **Rework share, 25 percent.** Its source is team capacity: a quarter of 800 hours is 200
  hours, and going above that means a quarter of a period going to fixes instead of new work.
- **Cycle time p90, 3.5 workdays.** Its source is the release gate itself: that gate's average
  wait is 1200 minutes, that is, 2.5 workdays. If the threshold were set below that, every
  change caught at the release gate would cross it, and the metric would report the gate
  arrangement's own outcome as if it were a defect.
- **Average feedback, 480 minutes.** One workday: the criterion that a defect reaches its owner
  within the same day.
- **Cycle time median and caught defect count have no threshold**, because they have no
  decision tied to them.

## The Metric That Changed a Decision

Two of the six metrics changed a decision. Leakage rate crossed its threshold, rising from 6.25
percent to 12.50 percent; rework share crossed its threshold too, rising from 22.75 percent to
29.00 percent. The remaining four either never crossed their threshold in either period or had
none.

The two metrics moved in the same direction but do not show the same thing. Leakage rate counts
defects, rework share weighs them in hours, and their rankings differ. In the first period, the
class eating the most rework hours is semantic mismatch: 48 hours, but only three defects.
Three defects out of eighty never rise to the top of any count-based list; weighed in hours, it
ranks first. In the second period, accessibility criterion jumps from 30 hours to 80 and takes
first place — all five of its defects are now found in production, and fixing them takes
sixteen hours instead of six. The decision, for this reason, comes out not as "do something"
but as "do this."

The remaining four metrics are this lesson's real finding. **Cycle time p90 dropped from 2.26
days to 1.23** — that is, this metric improved while quality got worse. The reason is written
in a single line of code: an escaped defect does not lengthen cycle time. The five changes that
used to wait at the release gate no longer wait there, because there is no longer a test there
to hold them. The median, meanwhile, did not move at all, staying at 1.05 days. **Deleting a
test always improves cycle time.**

Average feedback time only worsened by about three percent, from 386.8 minutes to 398.0 — while
leakage doubled. The reason is how close the two numbers are: the release gate used to report a
defect in 2221 minutes, an escaped defect in production is reported in 2400. **The average
cannot tell catching apart from missing.** Caught defect count also dropped from 75 to 70; read
on its own, this row looks like "fewer defects," but what it says is fewer catches.

The attention arithmetic gives the price of this: six metrics ask for 180 minutes of collection
and review per period, and 120 minutes of that changes no decision. **A metric that changes no
decision is noise** — and its cost is not just that 120 minutes. In the same period, cycle time
and caught defect count look like they improved together; a team looking at just those two
could read the decision that doubled leakage as a success.

## The Return on the Decision

Leakage rate's decision changes the inventory: a new test is written for the blind-spot class,
and that test's run cost is added to the next period's pipeline time. This worsens cycle time.
**Metrics are not independent of each other**; while one is improved, another pays for it, and
this is why no single metric is targeted alone.

Rework share's decision moves a defect class to an earlier stage, and its owner is whoever
built the gate arrangement. Cycle time's decision changes stage placement. A metric with no
decision tied to it has no owner; it gets read out as a number in the period meeting, no one
acts on it, and its cost is paid again every period.

## Summary

- Leakage rate, rework share, and cycle time were computed from the same defect set; rework
  here is a separate concept from the failback in the Resilience and Reliability course — the
  return to the primary copy after a takeover.
- A single pipeline-cheapening decision raised leakage rate from 6.25 percent to 12.50 percent
  and rework share from 22.75 percent to 29.00 percent; both crossed their threshold and
  produced a decision.
- The same decision dropped cycle time p90 from 2.26 days to 1.23, because an escaped defect
  does not lengthen cycle time; the median did not change at all, and caught defects dropped
  from 75 to 70.
- Average feedback time only worsened by three percent: because the time to notice in
  production (2400 min) is close to the release gate's wait (2221 min), the average conceals
  the miss.
- Two of the six metrics changed a decision; the remaining four ate 120 minutes of attention
  per period, and two pointed the wrong way. **A metric that changes no decision is noise.**

## Next Step

Metrics say one thing and do not say another. What they say: schema mismatch came up eight
times this period, boundary comparison thirteen times, and most rework hours pile up in a few
classes. What they do not say: why. All eight schema mismatches were fixed one by one, each one
paying its own rework hours again; if there is a single shared cause under those eight, no
metric shows it, because metrics count, they do not explain. A class recurring is not a defect,
it is a symptom, and going from symptom to cause needs a method. The next lesson builds that
method.
