Skip to content
Putting technology to work.
Insights to guide decisions and action.

Search articles

100% Coverage Still Does Not Stop Bugs: Guaranteeing Deliverable Test Quality with Mutation Testing in Custom Development

Table of contents · 7 items

Right after feeling reassured by receiving test results reporting 100% coverage, a bug crops up in production—a scene we have witnessed countless times during handovers and acceptance testing in custom development. The coverage figure is not lying. The problem is that the metric indicates only that code was executed, without guaranteeing that the tests properly validated what happened inside. Even if a test runs through the target code, bugs slip through if the crucial assert checks (verifying expected values) are weak.

As a technique for exposing this coverage trap, mutation testing has been re-evaluated in 2026. Its presence in articles like Zenn's Even 100% Coverage Cannot Stop Bugs: Shifting from Test Quantity to Test Quality and its entry into the Trial ring of Thoughtworks' Technology Radar Vol. 34 (April 2026) highlight this trend. A major driver is that as LLMs mass-generate tests, coverage has become even less reliable as an indicator of quality. When an AI-written test passes without actually validating anything, it is difficult for a human to spot the issue through visual review alone. When responsible for guaranteeing deliverable quality in custom development, we believe mutation testing serves as a test of the tests and represents one of the most practical solutions available today.

Why Coverage Does Not Guarantee Quality

Coverage is merely a metric that counts which lines and branches were traversed during test execution. A test like the following can still achieve 100% coverage:

// 対象: 割引額を計算する関数
function calcDiscount(price: number, rate: number): number {
  if (rate > 0.5) return price * 0.5;   // 上限50%
  return price * rate;
}

// "通るだけ" のテスト(カバレッジ100%だが検証が弱い)
test('calcDiscount runs', () => {
  calcDiscount(1000, 0.3);  // 呼ぶだけ
  calcDiscount(1000, 0.8);  // 呼ぶだけ
  expect(true).toBe(true);  // 何も検証していない
});

This test hits both branches and achieves 100% coverage, but because it does not validate the return value at all, rewriting return price * 0.5 to return price * 0.4 still results in a passing test. In other words, it cannot stop a single bug. Coverage is a classic example of Goodhart's Law—when a measure becomes a target, it ceases to be a good measure. The moment it becomes the target, pressure mounts to satisfy the number using hollow tests like this.

What Mutation Testing Actually Does

Mutation testing checks whether intentionally injecting small bugs (mutants) into the target code causes existing tests to detect them and fail. If detected, the mutant is marked as killed; if undetected and the test passes, it is marked as survived. This visualizes surviving mutants as test blind spots.

DimensionCode CoverageMutation Testing
What it measuresWhether code was executedWhether tests can detect bugs
Weak assertionsCannot detectDetected as survivors
Empty testsCan reach 100%Exposed by lower scores
LLM-generated testsCannot differentiate qualityCan quantify quality
Primary use caseUnderstanding execution scopeGuaranteeing validation capability

For example, mutations such as changing > to >=, swapping return a + b for return a - b, or inverting conditions are injected automatically in high volume, and the test suite's efficacy is reported as the mutation score (percentage killed). As seen in KAKEHASHI's case study, Validating the Quality of LLM-Generated Tests with Mutation Testing, an increasing number of organizations are adopting it to measure the effectiveness of AI-written tests. If custom development relies on AI to mass-produce tests, a mechanism to ensure their quality must accompany it. This perspective of ensuring AI-generated deliverable quality connects directly to Adopting QA Automation in Custom Development (GH Media).

Minimal Implementation Example (Stryker)

For JavaScript and TypeScript, Stryker Mutator is the standard choice. It runs directly on top of existing Jest or Vitest suites.

# 1) 導入
npm i -D @stryker-mutator/core @stryker-mutator/vitest-runner

# 2) 設定(stryker.conf.json の最小例)
#   - 重要モジュールだけを対象に、まず小さく始める
#   - しきい値で CI を制御(high=合格 / break=失敗)
cat > stryker.conf.json <<'JSON'
{
  "testRunner": "vitest",
  "mutate": ["src/domain/**/*.ts"],
  "thresholds": { "high": 80, "low": 60, "break": 60 }
}
JSON

# 3) 実行 → Mutation Score を出力
npx stryker run

The key is not applying it across all code from day one. Because mutation testing requires heavier execution than standard test suites, begin by narrowing focus to critical business logic where mistakes cause direct damage—such as pricing calculations, inventory management, and authorization checks—and enforce quality thresholds mechanically in CI using break. For setups in Vitest environments, Adopting Vitest 4.1 in Custom Development (GH Media) provides useful guidance.

How We Apply It in Custom Development: Turning "Tests Exist" into "Tests Stop Bugs"

Reporting during acceptance or review that "tests were written" or "coverage is at 90%" does not guarantee quality on its own. To make a higher-tier commitment for deliverables in custom development, we apply it in the following ways:

For core logic, we build a minimum mutation score threshold into contractual terms and CI pass criteria. CI fails unless code meets not only criteria like 80%+ coverage, but also 70%+ mutation score on critical modules. This establishes an environment where tests that merely run without verifying cannot pass. For one backend development client, inherited code had high coverage, but the pricing logic's mutation score was extremely low, uncovering numerous tests where assert never validated calculated amounts. Reinforcing these areas ensured subsequent releases were virtually free of billing-related bugs.

On projects where LLMs mass-produce tests, we standardize a step to run mutation testing immediately after generation and eliminate surviving mutants. This serves as a safeguard to prevent AI-generated tests from stopping at merely passing.

Common pitfalls to avoid

First, running it across the entire codebase at once causes execution times to explode. Because it is resource-intensive, limit the scope to core logic and offload CI execution to pull-request diffs or nightly runs. Second, aiming for a 100% mutation score. Equivalent mutants (mutations that are semantically identical and cannot be killed) inevitably appear, so operate with realistic thresholds (such as 70–80% for core logic). Third, leaving survivors unaddressed. Visualizing flaws is meaningless unless they are resolved, so survivors must always be triaged during reviews as either requiring added tests or being classified as equivalent. For broader quality assurance including E2E, please also read Automating QA with Playwright and AI in Custom Development (GH Media).

Conclusion: Shifting Metrics from Execution Rate to Detection Power

100% coverage does not mean everything was tested; it simply means every line was traversed. What stops bugs in production is not running code, but resilient tests capable of detecting changes. Mutation testing quantifies that detection power and makes the quality of LLM-generated tests transparent. When responsible for guaranteeing quality in custom development, establishing minimum mutation score thresholds on core logic, incorporating them into CI pass conditions, and eliminating surviving mutants offers a practical strategy to turn "tests exist" into "tests stop bugs."

If you are facing situations where coverage is high yet production bugs persist, or have concerns regarding the quality of AI-written tests, please reach out via our contact form. We can start by introducing mutation testing to your core logic.

Sources

Share this articleXFacebook
Kakeru Suzuki

Fascinated by the possibilities of technology, has had a deep interest in programming and digital art since student days

Turn this article's theme into your company's next step

Concrete steps forward for your organization.

We organize your desired architecture, legacy systems, and operational requirements to formulate your next steps toward execution.

  • Desired architecture
  • Integration with existing environments
  • Operational requirements
Consult on development & operations initiatives

You can consult with us from the initial conceptual stage. Details from this article will be carried over to the inquiry form.

Receive the latest articles by email