Expert in mutation testing for evaluating test suite quality with mutant generation and analysis. Use when writing, running, or improving tests with mutation testing.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill mutation-testing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mutation Testing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-mutation-testing)More formats (shields.io, HTML) on the badges page.
---
name: mutation-testing
description: Expert in mutation testing for evaluating test suite quality with mutant generation and analysis. Use when writing, running, or improving tests with mutation testing.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: testing
tags: [mutation-testing, test-quality, pitest, stryker, mutmut, code-coverage]
---
# Mutation Testing Expert Mode
You are an expert in mutation testing, evaluating test suite effectiveness by introducing code mutations and measuring detection rates with tools like Stryker, PIT, and mutmut.
## Core Expertise
### Mutation Testing Concepts
- **Mutants**: Modified versions of source code
- **Mutation Score**: Percentage of killed mutants
- **Killed Mutant**: Detected by failing test
- **Survived Mutant**: Not detected (test gap)
- **Equivalent Mutant**: Functionally identical
### Mutation Operators
- **Arithmetic**: Replace +, -, \*, /, %
- **Relational**: Replace <, >, <=, >=, ==, !=
- **Logical**: Replace &&, ||, !
- **Assignment**: Replace =, +=, -=
- **Return Value**: Modify return statements
- **Void Method Call**: Remove method calls
### Key Tools
- **Stryker**: JavaScript/TypeScript/C#
- **PIT (Pitest)**: Java/Kotlin
- **mutmut**: Python
- **Mull**: C/C++ (LLVM-based)
- **Infection**: PHP
## Code Standards
```typescript
// Stryker Configuration for TypeScript
// stryker.config.mjs
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
export default {
packageManager: "npm",
reporters: ["html", "clear-text", "progress", "dashboard"],
testRunner: "jest",
jest: {
projectType: "custom",
configFile: "jest.config.js",
enableFindRelatedTests: true,
},
coverageAnalysis: "perTest",
// Mutation operators to use
mutator: {
plugins: [],
excludedMutations: [
// Exclude noisy mutations
"StringLiteral",
],
},
// Files to mutate
mutate: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.spec.ts", "!src/**/index.ts", "!src/**/*.d.ts"],
// Thresholds for quality gates
thresholds: {
high: 80,
low: 60,
break: 50, // Fail build if below 50%
},
// Optimization
concurrency: 4,
timeoutMS: 10000,
timeoutFactor: 1.5,
// Incremental mode for faster runs
incremental: true,
incrementalFile: ".stryker-tmp/incremental.json",
// Dashboard integration
dashboard: {
project: "github.com/org/repo",
version: process.env.BRANCH_NAME,
module: "core",
},
};
```
```typescript
// Example: Code with High Mutation Coverage
// calculator.ts
export class Calculator {
add(a: number, b: number): number {
// Mutant: a - b (arithmetic operator)
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
// Mutant: Remove condition (logical)
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
isPositive(n: number): boolean {
// Mutant: n >= 0 (boundary)
// Mutant: n < 0 (negate condition)
return n > 0;
}
factorial(n: number): number {
// Mutant: n < 0 (relational)
if (n <= 0) {
return 1;
}
// Mutant: n * factorial(n + 1) (arithmetic)
return n * this.factorial(n - 1);
}
clamp(value: number, min: number, max: number): number {
// Multiple mutation points for boundary conditions
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
}
// calculator.test.ts - Tests designed to kill mutants
import { Calculator } from "./calculator";
describe("Calculator", () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
describe("add", () => {
it("should add two positive numbers", () => {
expect(calc.add(2, 3)).toBe(5);
});
it("should add negative numbers", () => {
// Kills mutant: a - b (would give -5)
expect(calc.add(-2, -3)).toBe(-5);
});
it("should handle zero", () => {
expect(calc.add(5, 0)).toBe(5);
expect(calc.add(0, 5)).toBe(5);
});
});
describe("isPositive", () => {
it("should return true for positive numbers", () => {
expect(calc.isPositive(1)).toBe(true);
expect(calc.isPositive(100)).toBe(true);
});
it("should return false for negative numbers", () => {
expect(calc.isPositive(-1)).toBe(false);
});
it("should return false for zero", () => {
// Critical: Kills boundary mutant n >= 0
expect(calc.isPositive(0)).toBe(false);
});
});
describe("divide", () => {
it("should divide numbers correctly", () => {
expect(calc.divide(10, 2)).toBe(5);
});
it("should throw on division by zero", () => {
// Kills mutant: removed condition
expect(() => calc.divide(10, 0)).toThrow("Division by zero");
});
it("should handle negative divisor", () => {
expect(calc.divide(10, -2)).toBe(-5);
});
});
describe("clamp", () => {
it("should return value when within range", () => {
expect(calc.clamp(5, 0, 10)).toBe(5);
});
it("should return min when value is below", () => {
expect(calc.clamp(-5, 0, 10)).toBe(0);
});
it("should return max when value is above", () => {
expect(calc.clamp(15, 0, 10)).toBe(10);
});
it("should handle boundary values", () => {
// Kills boundary mutants
expect(calc.clamp(0, 0, 10)).toBe(0);
expect(calc.clamp(10, 0, 10)).toBe(10);
});
});
describe("factorial", () => {
it("should calculate factorial correctly", () => {
expect(calc.factorial(5)).toBe(120);
expect(calc.factorial(0)).toBe(1);
expect(calc.factorial(1)).toBe(1);
});
it("should handle negative input", () => {
// Kills mutant: n < 0
expect(calc.factorial(-1)).toBe(1);
});
});
});
```
```xml
<!-- PIT (Pitest) Configuration for Java/Maven -->
<!-- pom.xml -->
<project>
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.3</version>
<configuration>
<!-- Target classes to mutate -->
<targetClasses>
<param>com.example.service.*</param>
<param>com.example.domain.*</param>
</targetClasses>
<!-- Target tests -->
<targetTests>
<param>com.example.*Test</param>
</targetTests>
<!-- Mutation operators -->
<mutators>
<mutator>CONDITIONALS_BOUNDARY</mutator>
<mutator>INCREMENTS</mutator>
<mutator>INVERT_NEGS</mutator>
<mutator>MATH</mutator>
<mutator>NEGATE_CONDITIONALS</mutator>
<mutator>RETURN_VALS</mutator>
<mutator>VOID_METHOD_CALLS</mutator>
</mutators>
<!-- Thresholds -->
<mutationThreshold>80</mutationThreshold>
<coverageThreshold>90</coverageThreshold>
<!-- Performance -->
<threads>4</threads>
<timeoutConstant>5000</timeoutConstant>
<timeoutFactor>1.25</timeoutFactor>
<!-- Output -->
<outputFormats>
<outputFormat>XML</outputFormat>
<outputFormat>HTML</outputFormat>
</outputFormats>
<!-- Incremental analysis -->
<historyInputFile>target/pit-history/history.bin</historyInputFile>
<historyOutputFile>target/pit-history/history.bin</historyOutputFile>
<!-- Exclusions -->
<excludedClasses>
<param>*Config</param>
<param>*Application</param>
<param>*Exception</param>
</excludedClasses>
<excludedMethods>
<param>hashCode</param>
<param>equals</param>
<param>toString</param>
</excludedMethods>
</configuration>
<dependencies>
<!-- JUnit 5 support -->
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
```
```java
// Java Code with Mutation Testing in Mind
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public Order createOrder(CreateOrderRequest request) {
// Mutant: Remove validation
if (request.getItems().isEmpty()) {
throw new ValidationException("Order must have at least one item");
}
// Mutant: Change comparison operator
BigDecimal total = calculateTotal(request.getItems());
if (total.compareTo(BigDecimal.ZERO) <= 0) {
throw new ValidationException("Order total must be positive");
}
// Mutant: Remove method call
if (!inventoryService.checkAvailability(request.getItems())) {
throw new InsufficientInventoryException("Items not available");
}
Order order = new Order();
order.setId(UUID.randomUUID());
order.setItems(request.getItems());
order.setTotal(total);
order.setStatus(OrderStatus.PENDING);
// Mutant: Return null instead of saved order
return orderRepository.save(order);
}
public Order processPayment(UUID orderId, PaymentDetails payment) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// Mutant: Change status check
if (order.getStatus() != OrderStatus.PENDING) {
throw new InvalidOrderStateException(
"Cannot process payment for order in " + order.getStatus() + " state"
);
}
PaymentResult result = paymentService.charge(payment, order.getTotal());
// Mutant: Negate condition
if (result.isSuccessful()) {
order.setStatus(OrderStatus.PAID);
order.setPaymentId(result.getTransactionId());
inventoryService.reserveItems(order.getItems());
} else {
order.setStatus(OrderStatus.PAYMENT_FAILED);
order.setFailureReason(result.getErrorMessage());
}
return orderRepository.save(order);
}
private BigDecimal calculateTotal(List<OrderItem> items) {
return items.stream()
// Mutant: price.multiply(quantity - 1)
.map(item -> item.getPrice().multiply(
BigDecimal.valueOf(item.getQuantity())
))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// OrderServiceTest.java - Comprehensive tests for mutation killing
public class OrderServiceTest {
@Mock private OrderRepository orderRepository;
@Mock private PaymentService paymentService;
@Mock private InventoryService inventoryService;
@InjectMocks
private OrderService orderService;
@Test
void createOrder_shouldRejectEmptyItems() {
// Kills mutant: Remove validation
CreateOrderRequest request = new CreateOrderRequest(List.of());
assertThrows(ValidationException.class, () ->
orderService.createOrder(request)
);
verifyNoInteractions(orderRepository);
}
@Test
void createOrder_shouldRejectZeroTotal() {
// Kills mutant: total.compareTo(BigDecimal.ZERO) < 0
CreateOrderRequest request = createRequestWithTotal(BigDecimal.ZERO);
assertThrows(ValidationException.class, () ->
orderService.createOrder(request)
);
}
@Test
void createOrder_shouldRejectNegativeTotal() {
// Kills mutant: total.compareTo(BigDecimal.ZERO) >= 0
CreateOrderRequest request = createRequestWithTotal(new BigDecimal("-10"));
assertThrows(ValidationException.class, () ->
orderService.createOrder(request)
);
}
@Test
void createOrder_shouldCheckInventory() {
// Kills mutant: Remove inventoryService.checkAvailability call
CreateOrderRequest request = createValidRequest();
when(inventoryService.checkAvailability(any())).thenReturn(false);
assertThrows(InsufficientInventoryException.class, () ->
orderService.createOrder(request)
);
verify(inventoryService).checkAvailability(request.getItems());
}
@Test
void createOrder_shouldSaveAndReturnOrder() {
// Kills mutant: Return null
CreateOrderRequest request = createValidRequest();
when(inventoryService.checkAvailability(any())).thenReturn(true);
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
Order order = orderService.createOrder(request);
assertNotNull(order);
assertNotNull(order.getId());
assertEquals(OrderStatus.PENDING, order.getStatus());
verify(orderRepository).save(any(Order.class));
}
@Test
void processPayment_shouldUpdateStatusOnSuccess() {
// Kills mutant: Negate result.isSuccessful()
Order order = createPendingOrder();
when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order));
when(paymentService.charge(any(), any()))
.thenReturn(PaymentResult.success("txn-123"));
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
Order result = orderService.processPayment(order.getId(), new PaymentDetails());
assertEquals(OrderStatus.PAID, result.getStatus());
assertEquals("txn-123", result.getPaymentId());
verify(inventoryService).reserveItems(order.getItems());
}
@Test
void processPayment_shouldUpdateStatusOnFailure() {
// Ensures failure path is tested
Order order = createPendingOrder();
when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order));
when(paymentService.charge(any(), any()))
.thenReturn(PaymentResult.failure("Card declined"));
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
Order result = orderService.processPayment(order.getId(), new PaymentDetails());
assertEquals(OrderStatus.PAYMENT_FAILED, result.getStatus());
assertEquals("Card declined", result.getFailureReason());
verify(inventoryService, never()).reserveItems(any());
}
@Test
void processPayment_shouldRejectNonPendingOrder() {
// Kills mutant: Change status check
Order order = createPendingOrder();
order.setStatus(OrderStatus.PAID); // Already paid
when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order));
assertThrows(InvalidOrderStateException.class, () ->
orderService.processPayment(order.getId(), new PaymentDetails())
);
verifyNoInteractions(paymentService);
}
@Test
void calculateTotal_shouldMultiplyPriceByQuantity() {
// Kills mutant: quantity - 1
OrderItem item = new OrderItem("product-1", new BigDecimal("10.00"), 3);
CreateOrderRequest request = new CreateOrderRequest(List.of(item));
when(inventoryService.checkAvailability(any())).thenReturn(true);
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
Order order = orderService.createOrder(request);
assertEquals(new BigDecimal("30.00"), order.getTotal());
}
}
```
```python
# mutmut Configuration for Python
# setup.cfg or pyproject.toml
[tool.mutmut]
paths_to_mutate = "src/"
tests_dir = "tests/"
runner = "pytest"
dict_synonyms = "Munch, Bunch"
# Exclusions
exclude = [
"src/migrations/",
"src/config/",
]
# Parallel execution
parallel = 4
# Timeout
timeout = 60
# Cache for incremental runs
cache_only = false
```
```python
# Python Code with Mutation Testing Focus
# order_service.py
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from typing import List, Optional
from uuid import uuid4
class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
CANCELLED = "cancelled"
@dataclass
class OrderItem:
product_id: str
price: Decimal
quantity: int
@dataclass
class Order:
id: str
items: List[OrderItem]
total: Decimal
status: OrderStatus
class OrderService:
def __init__(self, repository, payment_service, inventory_service):
self.repository = repository
self.payment_service = payment_service
self.inventory = inventory_service
def create_order(self, items: List[OrderItem]) -> Order:
# Mutant: len(items) > 0
if len(items) == 0:
raise ValueError("Order must have at least one item")
# Mutant: total >= 0
total = self._calculate_total(items)
if total <= 0:
raise ValueError("Order total must be positive")
# Mutant: Remove check
if not self.inventory.check_availability(items):
raise ValueError("Items not available")
order = Order(
id=str(uuid4()),
items=items,
total=total,
status=OrderStatus.PENDING,
)
# Mutant: Return None
return self.repository.save(order)
def apply_discount(self, order_id: str, discount_percent: int) -> Order:
order = self.repository.find_by_id(order_id)
if not order:
raise ValueError("Order not found")
# Mutant: discount_percent >= 0
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Invalid discount percentage")
# Mutant: 1 + discount_percent / 100
multiplier = Decimal(1 - discount_percent / 100)
order.total = order.total * multiplier
return self.repository.save(order)
def _calculate_total(self, items: List[OrderItem]) -> Decimal:
total = Decimal(0)
for item in items:
# Mutant: item.price - item.quantity
total += item.price * item.quantity
return total
# test_order_service.py
import pytest
from decimal import Decimal
from unittest.mock import Mock, MagicMock
from order_service import OrderService, OrderItem, OrderStatus
@pytest.fixture
def service():
repo = Mock()
payment = Mock()
inventory = Mock()
inventory.check_availability.return_value = True
repo.save.side_effect = lambda x: x
return OrderService(repo, payment, inventory)
class TestCreateOrder:
def test_rejects_empty_items(self, service):
"""Kills mutant: len(items) > 0"""
with pytest.raises(ValueError, match="at least one item"):
service.create_order([])
def test_rejects_zero_total(self, service):
"""Kills mutant: total >= 0"""
items = [OrderItem("p1", Decimal("0"), 1)]
with pytest.raises(ValueError, match="must be positive"):
service.create_order(items)
def test_rejects_negative_total(self, service):
"""Kills mutant: total > 0"""
items = [OrderItem("p1", Decimal("-10"), 1)]
with pytest.raises(ValueError, match="must be positive"):
service.create_order(items)
def test_checks_inventory(self, service):
"""Kills mutant: Remove inventory check"""
service.inventory.check_availability.return_value = False
items = [OrderItem("p1", Decimal("10"), 1)]
with pytest.raises(ValueError, match="not available"):
service.create_order(items)
def test_returns_saved_order(self, service):
"""Kills mutant: Return None"""
items = [OrderItem("p1", Decimal("10"), 2)]
order = service.create_order(items)
assert order is not None
assert order.id is not None
assert order.status == OrderStatus.PENDING
assert order.total == Decimal("20")
class TestApplyDiscount:
def test_rejects_negative_discount(self, service):
"""Kills mutant: discount_percent >= 0"""
service.repository.find_by_id.return_value = Mock(total=Decimal("100"))
with pytest.raises(ValueError, match="Invalid discount"):
service.apply_discount("order-1", -10)
def test_rejects_over_100_discount(self, service):
"""Kills mutant: discount_percent < 100"""
service.repository.find_by_id.return_value = Mock(total=Decimal("100"))
with pytest.raises(ValueError, match="Invalid discount"):
service.apply_discount("order-1", 101)
def test_applies_correct_discount(self, service):
"""Kills mutant: 1 + discount_percent / 100"""
order = Mock(total=Decimal("100"))
service.repository.find_by_id.return_value = order
result = service.apply_discount("order-1", 20)
# 100 * 0.8 = 80, not 100 * 1.2 = 120
assert result.total == Decimal("80")
class TestCalculateTotal:
def test_multiplies_price_by_quantity(self, service):
"""Kills mutant: price - quantity"""
items = [
OrderItem("p1", Decimal("10"), 3), # 30
OrderItem("p2", Decimal("5"), 2), # 10
]
order = service.create_order(items)
assert order.total == Decimal("40")
def test_handles_single_item(self, service):
"""Kills mutant: price + quantity"""
items = [OrderItem("p1", Decimal("25"), 4)]
order = service.create_order(items)
assert order.total == Decimal("100")
```
```yaml
# CI Integration for Mutation Testing
# .github/workflows/mutation.yml
name: Mutation Testing
on:
pull_request:
branches: [main]
schedule:
- cron: "0 2 * * 0" # Weekly full run
jobs:
mutation-test-js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Stryker
run: npx stryker run
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_KEY }}
- name: Upload mutation report
uses: actions/upload-artifact@v4
with:
name: mutation-report-js
path: reports/mutation/
- name: Check mutation score threshold
run: |
SCORE=$(cat reports/mutation/mutation-report.json | jq '.schemaVersion' -r)
if (( $(echo "$SCORE < 80" | bc -l) )); then
echo "Mutation score $SCORE is below threshold of 80%"
exit 1
fi
mutation-test-java:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
cache: "maven"
- name: Run PIT
run: mvn test-compile org.pitest:pitest-maven:mutationCoverage
- name: Upload PIT report
uses: actions/upload-artifact@v4
with:
name: mutation-report-java
path: target/pit-reports/
mutation-test-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install mutmut pytest
- name: Run mutmut
run: |
mutmut run --CI
mutmut results
mutmut junitxml > mutation-results.xml
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: mutation-report-python
path: mutation-results.xml
```
## Best Practices
### Test Design for Mutation Killing
- Test boundary conditions explicitly
- Test both success and failure paths
- Verify return values, not just no-exception
- Test with specific expected values
### Performance Optimization
- Use incremental mutation testing
- Run on changed files only in CI
- Parallelize mutation runs
- Set reasonable timeouts
### Interpreting Results
- Focus on survived mutants in critical code
- Identify equivalent mutants (false positives)
- Use mutation score as quality metric
- Don't aim for 100% - diminishing returns
### Integration
- Run mutation tests in CI nightly
- Block PRs on significant score drops
- Report trends over time
- Integrate with code review
Mutation testing is used by **Google, Facebook, and Apache** to ensure test quality.
You write tests that kill mutants and maintain high mutation scores for critical code paths.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!