--- title: "Reading mutation results and strengthening tests" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Reading mutation results and strengthening tests} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) source("engine.R") ``` The mutation score is only useful if you know how to read it. This article walks through three real scenarios — one for each operator class — showing what a surviving mutant looks like in the output, what it tells you about your tests, and exactly what to change. ## The output table Every `muttest::muttest()` run produces a table with one row per mutator/file pair: | Column | Meaning | | ------ | -------------------------------------------------------------------------------------------- | | K | **Killed** — mutants that caused at least one test to fail. Your tests caught the change. | | S | **Survived** — mutants that passed all tests. Your tests did not notice the change. | | E | **Errors** — mutants whose mutated code errored (parse or runtime). The mutation was still detected, so it counts as killed; it is tracked separately so you can see how it was caught. | | T | **Total** — mutants generated for this mutator/file pair. | | % | **Score** — `(K + E) / (K + S + E) × 100`. Errors count as detected; mutants with no test coverage are excluded. | A `✔` row means at least one mutant was killed. An `x` row means every mutant survived — your tests passed unchanged. ## What the score tells you | Score | Signal | | ----- | ---------------------------------------------------------------------------------------- | | 0% | Your assertions do not constrain the outcome. Tests pass no matter what. | | 1–99% | Some cases are well-tested; others are not. The survivors point at the gaps. | | 100% | Every generated mutant was caught. Your tests are robust against these specific changes. | A 100% score does not mean there are no bugs — it means there are no untested bugs *of the kinds the mutators probe*. Start with the survivors; they are the actionable signal. ## [The HTML report](https://jakubsob.github.io/muttest/report/muttest.html) For anything bigger than a couple of files, review survivors in the HTML report instead of the terminal. Run muttest with `JSONMutationReporter`, then render the report: ```r muttest(plan, reporter = JSONMutationReporter$new()) report() ``` The report opens filtered to survived mutants, overlays each mutant on its source line, and shows the exact diff that your tests failed to notice. [See a live example report](https://jakubsob.github.io/muttest/report/muttest.html). --- ```{muttest_examples} shipping, mad, access # nolint ``` --- ## Summary: three patterns, three fixes | Surviving mutant | Root cause | Fix | | ----------------------- | ------------------------------------------ | ------------------------------------------------------------------------- | | Comparison (`>=` → `>`) | Boundary value never tested | Add a test that passes the exact threshold value | | Arithmetic (`-` → `+`) | Assertion checks direction, not value | Replace `expect_gt/gte` with `expect_equal` and a computed expected value | | Logical (`\|\|` → `&&`) | Symmetric inputs (both true or both false) | Add a test with asymmetric inputs (one true, one false) | The feedback loop is: 1. Run muttest 2. Find survivors 3. Strengthen tests 4. Repeat Start with one file and one mutator preset. Look at what survives. The survivor names the exact mutant that your tests cannot distinguish — that is your actionable signal.