DEV Community

Dakota Wu
Dakota Wu

Posted on

Mutation Testing on a Budget: Free Models, Free Server, Real Test Quality

Your test suite is green. Your deployment is confident. Your users still find bugs. The gap is not in your code. It is in your tests. Mutation testing exposes that gap by breaking your code on purpose. You do not need expensive infrastructure to run it. Free models and a free server are enough.

The lie in a green suite

A passing test run only proves one thing. Your code behaves for the inputs you wrote. It says nothing about the assertions you forgot. Mutation testing measures test quality by injecting faults.

A mutant is a tiny change to your source. > becomes <. + becomes -. A condition flips from true to false. Then you run your test suite against the mutant. If the tests still pass, the mutant survived. A survivor means your tests missed a behavior. If the tests fail, the mutant was killed. Killed mutants mean your tests actually catch changes.

Why teams skip it

Full mutation testing is computationally expensive. A real codebase can produce thousands of mutants. Each mutant needs a full test run. That is why most teams never start.

Paid CI minutes add up quickly. Local runs freeze your machine. The result is a powerful technique that stays in textbooks.

The budget workflow

You can run a meaningful mutation cycle with three pieces. A small mutator script. Your existing test suite. A free server to run the matrix.

Here is the workflow:

  1. Extract the target function. Pick a pure function with real logic. Pure functions make mutants easy to reason about.
  2. Generate mutants. Use a free model to produce variant implementations. Or write a script that rewrites the source.
  3. Run the suite against each mutant. The free server runs the jobs in parallel.
  4. Collect survivors. A surviving mutant is a test gap. Add an assertion, then re-run.

A minimal mutator script

You do not need a heavy framework to start. Here is a compact Node.js script that generates mutants by swapping operators:

// mutate.js
import { readFileSync, writeFileSync } from "node:fs";

const source = readFileSync(process.argv[2], "utf8");
const mutants = [
  [">", "<"],
  ["<", ">"],
  ["===", "!=="],
  ["!==", "==="],
  ["+", "-"],
  ["-", "+"],
  ["&&", "||"],
  ["||", "&&"],
];

let index = 0;
for (const [from, to] of mutants) {
  if (!source.includes(from)) continue;
  const mutant = source.replace(from, to);
  writeFileSync(`mutant_${index++}.js`, mutant);
}
Enter fullscreen mode Exit fullscreen mode

This is deliberately simple. It replaces the first occurrence of each operator. A production tool uses AST traversal. This shows the concept clearly.

Run it on a sample function:

// discount.js
export function discount(price, code) {
  if (code === "SAVE10" && price > 100) return price * 0.9;
  return price;
}
Enter fullscreen mode Exit fullscreen mode

Generate mutants:

node mutate.js discount.js
ls mutant_*.js
Enter fullscreen mode Exit fullscreen mode

Then run your test suite against each mutant. The free server lets you run all of them at once.

Why use a model for mutant generation

Operator swapping is a good start. It misses many real bugs. A model can generate smarter mutants. It can change return values, reorder conditions, or remove edge cases.

Give the model your function and ask for twenty variants. Each variant should change one behavior. You get a richer mutation set in seconds. Free model access makes this step cost nothing.

Choosing the right function

Not every function deserves mutation testing. Use this decision table.

Scenario Use mutation testing? Why
Legacy module with no tests No Write characterization tests first
Pure business logic Yes High value, low cost
UI components Partial Focus on state transitions
One-off scripts No Not worth the overhead
Code with known bugs Yes Exposes missing assertions

Start with a pure function that has at least one branch and one arithmetic operation. That is the sweet spot.

Where free resources fit

Generating mutants is a language task. A free model can read a function and return twenty plausible variants in seconds. Running the test matrix is a compute task. A free server handles parallel execution without touching your laptop.

MonkeyCode offers free model access and a free server, which is enough for a small mutation cycle at the time of writing. That combination removes the two excuses most teams have: model cost and compute cost.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

A real run, step by step

Assume you have a function and a test file. Here is the command sequence:

# 1. Generate mutants
node mutate.js discount.js

# 2. Run the suite against the original
node --test discount.test.js

# 3. Run the suite against every mutant
for f in mutant_*.js; do
  cp "$f" discount.js
  node --test discount.test.js > "result_${f}.txt" 2>&1
done

# 4. Find survivors
grep -L "pass" result_*.txt
Enter fullscreen mode Exit fullscreen mode

The last command lists mutants that did not cause a test failure. Those are your gaps.

What a survivor teaches you

A surviving mutant tells you something specific. Your tests do not exercise that branch. Or your assertions are too loose. Or the output is never checked.

For example, if changing > to < does not fail, your tests probably only cover one side of the comparison. Add a case for the other side. If changing + to - does not fail, your assertions may only check the happy path.

Each survivor is a clue. Follow it with a new test case. Re-run the matrix. Watch the survivor die.

Limitations

  • Simple mutators miss complex bugs. Operator swaps cover a lot, but not everything.
  • Generated mutants can be invalid. Some rewrites break syntax. Filter them out.
  • Test flakiness pollutes results. Use deterministic tests only.
  • Not a replacement for code review. Mutation testing finds blind spots in tests, not design flaws.

Who should skip this

  • Teams with no test suite at all. Write some tests first.
  • Projects with heavy integration tests. Mutation testing works best at the unit level.
  • Developers who cannot act on survivors. A gap you ignore is a gap you keep.

Start with one function

Pick the ugliest pure function in your codebase. Generate mutants. Run the matrix. Look at the survivors. You will learn more about your tests in one hour than a week of green CI runs.

The free tier is enough to start. Your next test failure might be the one that saves you.

Top comments (0)