Runtime mutation injection for Python tests
Project description
mutation-testing
A Python mutation testing framework that validates test suite quality by injecting runtime mutations and checking whether your tests catch them.
Mutations are applied at runtime using AST pattern matching — source files are never modified.
How it works
- You define mutations: intentional code changes like replacing
+with-or>with>= - The framework injects each mutation into the running code by swapping the function's
__code__object - Your test suite runs against the mutated code
- If a test fails, the mutation was killed (good — your tests caught the bug)
- If all tests pass, the mutation survived (bad — your tests have a gap)
The mutation score is the percentage of mutations killed. A higher score means stronger tests.
Installation
Requires Python 3.10+.
pip install mutation-testing
# Or with uv
uv add mutation-testing
For development:
git clone https://github.com/YeahWick/mutation-testing.git
cd mutation-testing
pip install -e ".[dev]"
Quick start
1. Define mutations in YAML
Create a mutations.yaml file:
version: "1.0"
settings:
timeout: 30
targets:
- module: "calculator"
file: "src/calculator.py"
mutations:
- id: "add-001"
function: "add"
description: "Replace + with -"
original: "return a + b"
mutant: "return a - b"
- id: "pos-001"
function: "is_positive"
description: "Replace > with >="
original: "return x > 0"
mutant: "return x >= 0"
2. Write a test runner
from mutation_testing import MutationRunner
def run_tests() -> bool:
"""Return True if all tests pass."""
try:
assert calculator.add(2, 3) == 5
assert calculator.add(-1, 1) == 0
assert calculator.is_positive(5) is True
assert calculator.is_positive(-1) is False
return True
except AssertionError:
return False
runner = MutationRunner(run_tests)
report = runner.run_from_config("mutations.yaml")
3. Run it
cd example
python run_mutations.py
Output:
============================================================
MUTATION TESTING
============================================================
[✓] [add-001] Replace + with -: KILLED
[✓] [add-002] Replace + with *: KILLED
[✓] [sub-001] Replace - with +: KILLED
[✗] [pos-001] Replace > with >=: SURVIVED
[✓] [pos-002] Replace > with <: KILLED
[✓] [clamp-001] Off-by-one in lower bound: KILLED
[✗] [clamp-002] Off-by-one in upper bound: SURVIVED
============================================================
SUMMARY
============================================================
Total mutations: 7
Killed: 5
Survived: 2
Mutation Score: 71.4%
============================================================
SURVIVING MUTATIONS (improve your tests!):
- [pos-001] Replace > with >=
- [clamp-002] Off-by-one in upper bound
The surviving mutations tell you exactly where your tests are weak — in this case, missing boundary checks for is_positive(0) and clamp at its upper bound.
API
Defining mutations explicitly
Instead of YAML, you can define mutations in code:
from mutation_testing import MutationRunner, Mutation
runner = MutationRunner(run_tests)
report = runner.run(
mutations=[
Mutation(
id="add-001",
function="add",
original="return a + b",
mutant="return a - b",
description="Replace + with -",
),
],
module_name="calculator",
)
print(f"Score: {report.score:.1%}")
print(f"All killed: {report.all_killed}")
Core classes
| Class | Purpose |
|---|---|
Mutation |
Defines a single mutation (id, function, original pattern, mutant pattern) |
MutationRunner |
High-level runner — accepts a test function, runs mutations, prints results |
MutationReport |
Results summary with total, killed, survived, score, all_killed |
MutationConfig |
Loads mutation definitions from a YAML file |
MutationInjector |
Low-level engine that injects/restores mutations at runtime |
MutationResult |
Result of a single mutation (mutation + killed boolean) |
Low-level API
For direct control over injection:
from mutation_testing import MutationInjector
injector = MutationInjector()
# Inject a mutation
injector.inject("calculator", "add", "return a + b", "return a - b")
# Run your tests here...
# Restore the original
injector.restore("calculator", "add")
# Or restore everything at once
injector.restore_all()
Mutation coverage report
The coverage report tracks which of your test functions have corresponding mutations defined. This helps you ensure every test is validated by at least one mutation, and can fail CI when coverage drops below a threshold.
Configure in YAML
Add a coverage section to your mutations.yaml:
coverage:
threshold: 100.0 # minimum percentage of tests that must have mutations
fail_under: true # exit non-zero if threshold not met
test_paths:
- "tests"
test_mappings: # optional: override the default naming convention
test_boundary_check:
- "clamp"
- "is_positive"
By default, tests are mapped to source functions by naming convention — test_add maps to add, test_is_positive maps to is_positive. Use test_mappings when a test name doesn't match its source function.
Generate the report
from mutation_testing import MutationRunner
runner = MutationRunner(run_tests)
coverage = runner.coverage_from_config("mutations.yaml")
# Check results programmatically
if not coverage.meets_threshold:
sys.exit(1)
Or use the standalone function:
from mutation_testing import generate_coverage_report, print_coverage_report
report = generate_coverage_report(
test_paths=["tests"],
mutation_config_path="mutations.yaml",
threshold=100.0,
)
print_coverage_report(report)
# JSON output for CI integration
print(report.to_json())
Output
============================================================
MUTATION COVERAGE REPORT
============================================================
[✓] test_add
functions: add
mutations: 2 (add-001, add-002)
[✓] test_subtract
functions: subtract
mutations: 1 (sub-001)
[✗] test_multiply
functions: multiply
mutations: 0
============================================================
COVERAGE SUMMARY
============================================================
Total tests: 3
Covered: 2
Uncovered: 1
Coverage: 66.7%
Threshold: 100.0%
Status: FAIL
============================================================
UNCOVERED TESTS (add mutations for these!):
- test_multiply -> multiply
CI usage
Use the exit code to fail CI pipelines:
coverage = runner.coverage_from_config("mutations.yaml")
sys.exit(0 if coverage.meets_threshold else 1)
Coverage classes
| Class | Purpose |
|---|---|
CoverageReport |
Full report with total_tests, covered_tests, coverage_percent, meets_threshold, all_covered |
TestCoverage |
Per-test status with test_name, mapped_functions, mutations, covered |
CoverageConfig |
YAML settings: threshold, fail_under, test_paths, test_mappings |
Supported mutation patterns
The AST pattern matcher supports any valid Python expression or statement:
- Arithmetic operators:
+↔-,*,/,// - Comparison operators:
>↔>=,<↔<=,==↔!= - Boolean operators:
and↔or - Return values:
return x→return None,return -x - Any expression parseable as Python
Patterns are matched structurally via AST, so whitespace and formatting differences are ignored.
Project structure
mutation_testing/
├── __init__.py # Public API exports
├── core.py # AST pattern matching, injection engine, Mutation/MutationResult
├── config.py # YAML configuration loader
├── coverage.py # Mutation coverage reporting
└── runner.py # MutationRunner and MutationReport
example/
├── src/calculator.py # Sample module under test
├── tests/test_calculator.py # Sample test suite
├── mutations.yaml # Sample mutation config
└── run_mutations.py # Example runner script
Key concepts
| Term | Meaning |
|---|---|
| Mutation | An intentional code change (e.g., + → -) |
| Killed | Tests detected the mutation (test failed) |
| Survived | Tests passed despite the mutation — tests need improvement |
| Mutation score | Killed / Total (higher is better) |
| Mutation coverage | Percentage of tests that have at least one mutation defined |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mutation_testing-0.1.0.tar.gz.
File metadata
- Download URL: mutation_testing-0.1.0.tar.gz
- Upload date:
- Size: 43.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6739bcbf9dc1f877930021cb2d07bef0c6b4fc9cd11146b001075ed3bd51cb0
|
|
| MD5 |
30381f7c8ad75d38a4aae9378fa4f466
|
|
| BLAKE2b-256 |
a6ace17f9ef373d2f7d975d68a0f979681a89bb3483e5841e4ddfd1bcaa61b09
|
Provenance
The following attestation bundles were made for mutation_testing-0.1.0.tar.gz:
Publisher:
publish.yml on YeahWick/mutation-testing
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mutation_testing-0.1.0.tar.gz -
Subject digest:
d6739bcbf9dc1f877930021cb2d07bef0c6b4fc9cd11146b001075ed3bd51cb0 - Sigstore transparency entry: 1001672926
- Sigstore integration time:
-
Permalink:
YeahWick/mutation-testing@08be81367f27fc0e1396330483cd6da5b4e473ed -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/YeahWick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@08be81367f27fc0e1396330483cd6da5b4e473ed -
Trigger Event:
release
-
Statement type:
File details
Details for the file mutation_testing-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mutation_testing-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec597a2b172c10796497d1bb47ee91846f22931b2a4ee8d6ad231d2c869cc19e
|
|
| MD5 |
cbd47db34e02b85e13dd870632697139
|
|
| BLAKE2b-256 |
4e6870a9174edb2b927fd1d9982fad33fc597cb1046c51f28c1b35497431bce8
|
Provenance
The following attestation bundles were made for mutation_testing-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on YeahWick/mutation-testing
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mutation_testing-0.1.0-py3-none-any.whl -
Subject digest:
ec597a2b172c10796497d1bb47ee91846f22931b2a4ee8d6ad231d2c869cc19e - Sigstore transparency entry: 1001672935
- Sigstore integration time:
-
Permalink:
YeahWick/mutation-testing@08be81367f27fc0e1396330483cd6da5b4e473ed -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/YeahWick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@08be81367f27fc0e1396330483cd6da5b4e473ed -
Trigger Event:
release
-
Statement type: