mutation-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMutation Testing
变异测试
Overview
概述
Mutation testing assesses test suite quality by introducing small changes (mutations) to source code and verifying that tests fail. If tests don't catch a mutation, it indicates gaps in test coverage or test quality. This technique helps identify weak or ineffective tests.
变异测试通过在源代码中引入微小变更(变异)并验证测试是否会失败,来评估测试套件的质量。如果测试未能捕获某个变异,说明测试覆盖率或测试质量存在漏洞。该技术有助于识别薄弱或无效的测试。
When to Use
适用场景
- Evaluating test suite effectiveness
- Finding untested code paths
- Improving test quality metrics
- Validating critical business logic is well-tested
- Identifying redundant or weak tests
- Measuring real test coverage beyond line coverage
- Ensuring tests actually verify behavior
- 评估测试套件的有效性
- 发现未测试的代码路径
- 提升测试质量指标
- 验证关键业务逻辑是否得到充分测试
- 识别冗余或薄弱的测试
- 衡量超出代码行覆盖率的真实测试覆盖率
- 确保测试实际验证了业务行为
Key Concepts
核心概念
- Mutant: Modified version of code with small change
- Killed: Test fails when mutation is introduced (good)
- Survived: Test passes despite mutation (test gap)
- Equivalent: Mutation that doesn't change behavior
- Mutation Score: Percentage of mutants killed
- Mutation Operators: Types of changes (arithmetic, conditional, etc.)
- 变异体(Mutant):经过微小修改的代码版本
- 被杀死(Killed):引入变异后测试失败(理想情况)
- 存活(Survived):引入变异后测试仍通过(存在测试漏洞)
- 等价(Equivalent):不会改变代码行为的变异
- 变异分数(Mutation Score):被杀死的变异体占比
- 变异算子(Mutation Operators):变更的类型(算术运算、条件判断等)
Instructions
操作指南
1. Stryker for JavaScript/TypeScript
1. 针对JavaScript/TypeScript的Stryker
bash
undefinedbash
undefinedInstall Stryker
安装Stryker
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
Initialize configuration
初始化配置
npx stryker init
npx stryker init
Run mutation testing
运行变异测试
npx stryker run
```javascript
// stryker.conf.json
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"packageManager": "npm",
"reporters": ["html", "clear-text", "progress", "dashboard"],
"testRunner": "jest",
"jest": {
"projectType": "custom",
"configFile": "jest.config.js",
"enableFindRelatedTests": true
},
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/*.test.ts"
],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
// Example source code
// src/calculator.ts
export class Calculator {
add(a: number, b: number): number {
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 {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
isPositive(n: number): boolean {
return n > 0;
}
}
// ❌ Weak tests - mutations will survive
describe('Calculator - Weak Tests', () => {
const calc = new Calculator();
test('add returns a number', () => {
const result = calc.add(2, 3);
expect(typeof result).toBe('number');
// This test won't catch mutations like: return a - b; or return a * b;
});
test('divide with non-zero divisor', () => {
expect(() => calc.divide(10, 2)).not.toThrow();
// Doesn't verify the actual result!
});
});
// ✅ Strong tests - will kill mutations
describe('Calculator - Strong Tests', () => {
const calc = new Calculator();
describe('add', () => {
test('adds two positive numbers', () => {
expect(calc.add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(calc.add(-2, -3)).toBe(-5);
});
test('adds zero', () => {
expect(calc.add(5, 0)).toBe(5);
expect(calc.add(0, 5)).toBe(5);
});
});
describe('subtract', () => {
test('subtracts numbers correctly', () => {
expect(calc.subtract(5, 3)).toBe(2);
expect(calc.subtract(3, 5)).toBe(-2);
});
});
describe('multiply', () => {
test('multiplies numbers', () => {
expect(calc.multiply(3, 4)).toBe(12);
expect(calc.multiply(-2, 3)).toBe(-6);
});
test('multiply by zero', () => {
expect(calc.multiply(5, 0)).toBe(0);
});
});
describe('divide', () => {
test('divides numbers correctly', () => {
expect(calc.divide(10, 2)).toBe(5);
expect(calc.divide(7, 2)).toBe(3.5);
});
test('throws error on division by zero', () => {
expect(() => calc.divide(10, 0)).toThrow('Division by zero');
});
});
describe('isPositive', () => {
test('returns true for positive numbers', () => {
expect(calc.isPositive(1)).toBe(true);
expect(calc.isPositive(100)).toBe(true);
});
test('returns false for zero and negative', () => {
expect(calc.isPositive(0)).toBe(false);
expect(calc.isPositive(-1)).toBe(false);
});
});
});npx stryker run
```javascript
// stryker.conf.json
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"packageManager": "npm",
"reporters": ["html", "clear-text", "progress", "dashboard"],
"testRunner": "jest",
"jest": {
"projectType": "custom",
"configFile": "jest.config.js",
"enableFindRelatedTests": true
},
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/*.test.ts"
],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
// 示例源代码
// src/calculator.ts
export class Calculator {
add(a: number, b: number): number {
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 {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
isPositive(n: number): boolean {
return n > 0;
}
}
// ❌ 薄弱测试 - 变异体将存活
describe('Calculator - Weak Tests', () => {
const calc = new Calculator();
test('add returns a number', () => {
const result = calc.add(2, 3);
expect(typeof result).toBe('number');
// 该测试无法捕获类似 return a - b; 或 return a * b; 这样的变异
});
test('divide with non-zero divisor', () => {
expect(() => calc.divide(10, 2)).not.toThrow();
// 未验证实际结果!
});
});
// ✅ 健壮测试 - 可杀死变异体
describe('Calculator - Strong Tests', () => {
const calc = new Calculator();
describe('add', () => {
test('adds two positive numbers', () => {
expect(calc.add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(calc.add(-2, -3)).toBe(-5);
});
test('adds zero', () => {
expect(calc.add(5, 0)).toBe(5);
expect(calc.add(0, 5)).toBe(5);
});
});
describe('subtract', () => {
test('subtracts numbers correctly', () => {
expect(calc.subtract(5, 3)).toBe(2);
expect(calc.subtract(3, 5)).toBe(-2);
});
});
describe('multiply', () => {
test('multiplies numbers', () => {
expect(calc.multiply(3, 4)).toBe(12);
expect(calc.multiply(-2, 3)).toBe(-6);
});
test('multiply by zero', () => {
expect(calc.multiply(5, 0)).toBe(0);
});
});
describe('divide', () => {
test('divides numbers correctly', () => {
expect(calc.divide(10, 2)).toBe(5);
expect(calc.divide(7, 2)).toBe(3.5);
});
test('throws error on division by zero', () => {
expect(() => calc.divide(10, 0)).toThrow('Division by zero');
});
});
describe('isPositive', () => {
test('returns true for positive numbers', () => {
expect(calc.isPositive(1)).toBe(true);
expect(calc.isPositive(100)).toBe(true);
});
test('returns false for zero and negative', () => {
expect(calc.isPositive(0)).toBe(false);
expect(calc.isPositive(-1)).toBe(false);
});
});
});2. PITest for Java
2. 针对Java的PITest
xml
<!-- pom.xml -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.14.2</version>
<configuration>
<targetClasses>
<param>com.example.service.*</param>
</targetClasses>
<targetTests>
<param>com.example.service.*Test</param>
</targetTests>
<mutators>
<mutator>DEFAULTS</mutator>
</mutators>
<outputFormats>
<outputFormat>HTML</outputFormat>
<outputFormat>XML</outputFormat>
</outputFormats>
<timestampedReports>false</timestampedReports>
<mutationThreshold>80</mutationThreshold>
<coverageThreshold>90</coverageThreshold>
</configuration>
</plugin>bash
undefinedxml
<!-- pom.xml -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.14.2</version>
<configuration>
<targetClasses>
<param>com.example.service.*</param>
</targetClasses>
<targetTests>
<param>com.example.service.*Test</param>
</targetTests>
<mutators>
<mutator>DEFAULTS</mutator>
</mutators>
<outputFormats>
<outputFormat>HTML</outputFormat>
<outputFormat>XML</outputFormat>
</outputFormats>
<timestampedReports>false</timestampedReports>
<mutationThreshold>80</mutationThreshold>
<coverageThreshold>90</coverageThreshold>
</configuration>
</plugin>bash
undefinedRun mutation testing
运行变异测试
mvn org.pitest:pitest-maven:mutationCoverage
```java
// src/main/java/OrderValidator.java
public class OrderValidator {
public boolean isValidOrder(Order order) {
if (order == null) {
return false;
}
if (order.getItems().isEmpty()) {
return false;
}
if (order.getTotal() <= 0) {
return false;
}
return true;
}
public double calculateDiscount(double total, String customerTier) {
if (customerTier.equals("GOLD")) {
return total * 0.2;
} else if (customerTier.equals("SILVER")) {
return total * 0.1;
}
return 0;
}
public int categorizeOrderSize(int itemCount) {
if (itemCount <= 5) {
return 1; // Small
} else if (itemCount <= 20) {
return 2; // Medium
} else {
return 3; // Large
}
}
}
// ❌ Weak tests that allow mutations to survive
@Test
public void testOrderValidation_Weak() {
OrderValidator validator = new OrderValidator();
Order order = new Order();
order.addItem(new Item("Product", 10.0));
order.setTotal(10.0);
// Only tests one scenario
assertTrue(validator.isValidOrder(order));
}
// ✅ Strong tests that kill mutations
public class OrderValidatorTest {
private OrderValidator validator;
@Before
public void setUp() {
validator = new OrderValidator();
}
@Test
public void isValidOrder_withNullOrder_returnsFalse() {
assertFalse(validator.isValidOrder(null));
}
@Test
public void isValidOrder_withEmptyItems_returnsFalse() {
Order order = new Order();
order.setTotal(10.0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withZeroTotal_returnsFalse() {
Order order = new Order();
order.addItem(new Item("Product", 0));
order.setTotal(0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withNegativeTotal_returnsFalse() {
Order order = new Order();
order.addItem(new Item("Product", -10.0));
order.setTotal(-10.0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withValidOrder_returnsTrue() {
Order order = new Order();
order.addItem(new Item("Product", 10.0));
order.setTotal(10.0);
assertTrue(validator.isValidOrder(order));
}
@Test
public void calculateDiscount_goldTier_returns20Percent() {
assertEquals(20.0, validator.calculateDiscount(100.0, "GOLD"), 0.01);
}
@Test
public void calculateDiscount_silverTier_returns10Percent() {
assertEquals(10.0, validator.calculateDiscount(100.0, "SILVER"), 0.01);
}
@Test
public void calculateDiscount_regularTier_returnsZero() {
assertEquals(0.0, validator.calculateDiscount(100.0, "BRONZE"), 0.01);
}
@Test
public void categorizeOrderSize_smallOrder() {
assertEquals(1, validator.categorizeOrderSize(3));
assertEquals(1, validator.categorizeOrderSize(5));
}
@Test
public void categorizeOrderSize_mediumOrder() {
assertEquals(2, validator.categorizeOrderSize(6));
assertEquals(2, validator.categorizeOrderSize(20));
}
@Test
public void categorizeOrderSize_largeOrder() {
assertEquals(3, validator.categorizeOrderSize(21));
assertEquals(3, validator.categorizeOrderSize(100));
}
// Test boundary conditions
@Test
public void categorizeOrderSize_boundaries() {
assertEquals(1, validator.categorizeOrderSize(5));
assertEquals(2, validator.categorizeOrderSize(6));
assertEquals(2, validator.categorizeOrderSize(20));
assertEquals(3, validator.categorizeOrderSize(21));
}
}mvn org.pitest:pitest-maven:mutationCoverage
```java
// src/main/java/OrderValidator.java
public class OrderValidator {
public boolean isValidOrder(Order order) {
if (order == null) {
return false;
}
if (order.getItems().isEmpty()) {
return false;
}
if (order.getTotal() <= 0) {
return false;
}
return true;
}
public double calculateDiscount(double total, String customerTier) {
if (customerTier.equals("GOLD")) {
return total * 0.2;
} else if (customerTier.equals("SILVER")) {
return total * 0.1;
}
return 0;
}
public int categorizeOrderSize(int itemCount) {
if (itemCount <= 5) {
return 1; // Small
} else if (itemCount <= 20) {
return 2; // Medium
} else {
return 3; // Large
}
}
}
// ❌ 薄弱测试,允许变异体存活
@Test
public void testOrderValidation_Weak() {
OrderValidator validator = new OrderValidator();
Order order = new Order();
order.addItem(new Item("Product", 10.0));
order.setTotal(10.0);
// 仅测试一种场景
assertTrue(validator.isValidOrder(order));
}
// ✅ 健壮测试,可杀死变异体
public class OrderValidatorTest {
private OrderValidator validator;
@Before
public void setUp() {
validator = new OrderValidator();
}
@Test
public void isValidOrder_withNullOrder_returnsFalse() {
assertFalse(validator.isValidOrder(null));
}
@Test
public void isValidOrder_withEmptyItems_returnsFalse() {
Order order = new Order();
order.setTotal(10.0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withZeroTotal_returnsFalse() {
Order order = new Order();
order.addItem(new Item("Product", 0));
order.setTotal(0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withNegativeTotal_returnsFalse() {
Order order = new Order();
order.addItem(new Item("Product", -10.0));
order.setTotal(-10.0);
assertFalse(validator.isValidOrder(order));
}
@Test
public void isValidOrder_withValidOrder_returnsTrue() {
Order order = new Order();
order.addItem(new Item("Product", 10.0));
order.setTotal(10.0);
assertTrue(validator.isValidOrder(order));
}
@Test
public void calculateDiscount_goldTier_returns20Percent() {
assertEquals(20.0, validator.calculateDiscount(100.0, "GOLD"), 0.01);
}
@Test
public void calculateDiscount_silverTier_returns10Percent() {
assertEquals(10.0, validator.calculateDiscount(100.0, "SILVER"), 0.01);
}
@Test
public void calculateDiscount_regularTier_returnsZero() {
assertEquals(0.0, validator.calculateDiscount(100.0, "BRONZE"), 0.01);
}
@Test
public void categorizeOrderSize_smallOrder() {
assertEquals(1, validator.categorizeOrderSize(3));
assertEquals(1, validator.categorizeOrderSize(5));
}
@Test
public void categorizeOrderSize_mediumOrder() {
assertEquals(2, validator.categorizeOrderSize(6));
assertEquals(2, validator.categorizeOrderSize(20));
}
@Test
public void categorizeOrderSize_largeOrder() {
assertEquals(3, validator.categorizeOrderSize(21));
assertEquals(3, validator.categorizeOrderSize(100));
}
// 测试边界条件
@Test
public void categorizeOrderSize_boundaries() {
assertEquals(1, validator.categorizeOrderSize(5));
assertEquals(2, validator.categorizeOrderSize(6));
assertEquals(2, validator.categorizeOrderSize(20));
assertEquals(3, validator.categorizeOrderSize(21));
}
}3. mutmut for Python
3. 针对Python的mutmut
bash
undefinedbash
undefinedInstall mutmut
安装mutmut
pip install mutmut
pip install mutmut
Run mutation testing
运行变异测试
mutmut run
mutmut run
Show results
查看结果
mutmut results
mutmut results
Show specific mutant
查看特定变异体
mutmut show 1
mutmut show 1
Apply mutation to see what changed
应用变异以查看变更内容
mutmut apply 1
```pythonmutmut apply 1
```pythonsrc/string_utils.py
src/string_utils.py
def is_palindrome(s: str) -> bool:
"""Check if string is palindrome."""
clean = ''.join(c.lower() for c in s if c.isalnum())
return clean == clean[::-1]
def count_words(text: str) -> int:
"""Count words in text."""
if not text:
return 0
return len(text.split())
def truncate(text: str, max_length: int) -> str:
"""Truncate text to max length."""
if len(text) <= max_length:
return text
return text[:max_length] + "..."
def is_palindrome(s: str) -> bool:
"""Check if string is palindrome."""
clean = ''.join(c.lower() for c in s if c.isalnum())
return clean == clean[::-1]
def count_words(text: str) -> int:
"""Count words in text."""
if not text:
return 0
return len(text.split())
def truncate(text: str, max_length: int) -> str:
"""Truncate text to max length."""
if len(text) <= max_length:
return text
return text[:max_length] + "..."
❌ Weak tests
❌ 薄弱测试
def test_palindrome_basic():
"""Weak: Only tests one case."""
assert is_palindrome("racecar") == True
def test_palindrome_basic():
"""Weak: Only tests one case."""
assert is_palindrome("racecar") == True
✅ Strong tests that will catch mutations
✅ 健壮测试,可捕获变异
def test_is_palindrome_simple():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
def test_is_palindrome_with_spaces():
assert is_palindrome("race car") == True
assert is_palindrome("not a palindrome") == False
def test_is_palindrome_with_punctuation():
assert is_palindrome("A man, a plan, a canal: Panama") == True
def test_is_palindrome_case_insensitive():
assert is_palindrome("RaceCar") == True
assert is_palindrome("Racecar") == True
def test_is_palindrome_empty():
assert is_palindrome("") == True
def test_is_palindrome_single_char():
assert is_palindrome("a") == True
def test_count_words_basic():
assert count_words("hello world") == 2
assert count_words("one") == 1
def test_count_words_multiple_spaces():
assert count_words("hello world") == 2
assert count_words(" leading spaces") == 2
def test_count_words_empty():
assert count_words("") == 0
assert count_words(" ") == 0
def test_truncate_short_text():
assert truncate("hello", 10) == "hello"
def test_truncate_exact_length():
assert truncate("hello", 5) == "hello"
def test_truncate_long_text():
result = truncate("hello world", 5)
assert result == "hello..."
assert len(result) == 8 # 5 + "..."
def test_truncate_zero_length():
assert truncate("hello", 0) == "..."
undefineddef test_is_palindrome_simple():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
def test_is_palindrome_with_spaces():
assert is_palindrome("race car") == True
assert is_palindrome("not a palindrome") == False
def test_is_palindrome_with_punctuation():
assert is_palindrome("A man, a plan, a canal: Panama") == True
def test_is_palindrome_case_insensitive():
assert is_palindrome("RaceCar") == True
assert is_palindrome("Racecar") == True
def test_is_palindrome_empty():
assert is_palindrome("") == True
def test_is_palindrome_single_char():
assert is_palindrome("a") == True
def test_count_words_basic():
assert count_words("hello world") == 2
assert count_words("one") == 1
def test_count_words_multiple_spaces():
assert count_words("hello world") == 2
assert count_words(" leading spaces") == 2
def test_count_words_empty():
assert count_words("") == 0
assert count_words(" ") == 0
def test_truncate_short_text():
assert truncate("hello", 10) == "hello"
def test_truncate_exact_length():
assert truncate("hello", 5) == "hello"
def test_truncate_long_text():
result = truncate("hello world", 5)
assert result == "hello..."
assert len(result) == 8 # 5 + "..."
def test_truncate_zero_length():
assert truncate("hello", 0) == "..."
undefined4. Mutation Testing Reports
4. 变异测试报告
bash
undefinedbash
undefinedStryker HTML report shows:
Stryker HTML报告包含:
- Mutation score: 85.5%
- 变异分数:85.5%
- Mutants killed: 94
- 被杀死的变异体:94个
- Mutants survived: 16
- 存活的变异体:16个
- Mutants timeout: 0
- 超时的变异体:0个
- Mutants no coverage: 10
- 无覆盖的变异体:10个
Example mutations:
示例变异:
❌ Survived: Changed > to >= in isPositive
❌ 存活:将isPositive中的>改为>=
No test checks boundary condition
没有测试检查该边界条件
✅ Killed: Changed + to - in add method
✅ 被杀死:将add方法中的+改为-
Test expects specific result
测试验证了具体结果
❌ Survived: Removed if condition check
❌ 存活:移除了if条件检查
Missing test for that edge case
缺少针对该边缘场景的测试
undefinedundefinedCommon Mutation Operators
常见变异算子
Arithmetic Mutations
算术运算变异
- →
+,-,*/ - →
-,+,*/ - →
*,+,-/ - →
/,+,-*
- →
+,-,*/ - →
-,+,*/ - →
*,+,-/ - →
/,+,-*
Conditional Mutations
条件判断变异
- →
>,>=,<== - →
<,<=,>== - →
==!= - →
&&|| - →
||&&
- →
>,>=,<== - →
<,<=,>== - →
==!= - →
&&|| - →
||&&
Return Value Mutations
返回值变异
- →
return truereturn false - →
return xreturn x + 1 - → Remove return statement
return
- →
return truereturn false - →
return xreturn x + 1 - → 移除return语句
return
Statement Mutations
语句变异
- Remove method calls
- Remove conditional blocks
- Remove increments/decrements
- 移除方法调用
- 移除条件块
- 移除自增/自减操作
Improving Mutation Score
提升变异分数
typescript
// Low mutation score example
function processUser(user: User): boolean {
if (user.age >= 18) {
user.isAdult = true;
sendWelcomeEmail(user);
return true;
}
return false;
}
// ❌ Weak test - Mutation: >= to > survives
test('processes adult user', () => {
const user = { age: 25 };
expect(processUser(user)).toBe(true);
});
// ✅ Strong test - Catches >= to > mutation
test('processes user who is exactly 18', () => {
const user = { age: 18 };
expect(processUser(user)).toBe(true);
expect(user.isAdult).toBe(true);
});
test('rejects user who is 17', () => {
const user = { age: 17 };
expect(processUser(user)).toBe(false);
expect(user.isAdult).toBeUndefined();
});typescript
undefinedBest Practices
低变异分数示例
✅ DO
—
- Target critical business logic for mutation testing
- Aim for 80%+ mutation score on important code
- Review survived mutants to improve tests
- Mark equivalent mutants to exclude them
- Use mutation testing in CI for critical modules
- Test boundary conditions thoroughly
- Verify actual behavior, not just code execution
function processUser(user: User): boolean {
if (user.age >= 18) {
user.isAdult = true;
sendWelcomeEmail(user);
return true;
}
return false;
}
❌ DON'T
❌ 薄弱测试 - 将>=改为>的变异体存活
- Expect 100% mutation score everywhere
- Run mutation testing on all code (too slow)
- Ignore equivalent mutants
- Test getters/setters with mutations
- Run mutations on generated code
- Skip mutation testing on complex logic
- Focus only on line coverage
test('processes adult user', () => {
const user = { age: 25 };
expect(processUser(user)).toBe(true);
});
Tools
✅ 健壮测试 - 可捕获>=改为>的变异
- JavaScript/TypeScript: Stryker Mutator
- Java: PITest, Major
- Python: mutmut, Cosmic Ray
- C#: Stryker.NET
- Ruby: Mutant
- PHP: Infection
test('processes user who is exactly 18', () => {
const user = { age: 18 };
expect(processUser(user)).toBe(true);
expect(user.isAdult).toBe(true);
});
test('rejects user who is 17', () => {
const user = { age: 17 };
expect(processUser(user)).toBe(false);
expect(user.isAdult).toBeUndefined();
});
undefinedIntegration with CI
最佳实践
—
✅ 建议
yaml
undefined- 针对关键业务逻辑开展变异测试
- 重要代码的变异分数目标设为80%以上
- 分析存活的变异体以优化测试
- 标记等价变异体并排除它们
- 在CI流程中针对关键模块运行变异测试
- 全面测试边界条件
- 验证实际业务行为,而非仅代码执行
.github/workflows/mutation-testing.yml
❌ 避免
name: Mutation Testing
on:
pull_request:
paths:
- 'src/**'
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx stryker run
- name: Check mutation score
run: |
SCORE=$(jq '.mutationScore' stryker-reports/mutation-score.json)
if (( $(echo "$SCORE < 80" | bc -l) )); then
echo "Mutation score $SCORE% is below threshold"
exit 1
fi
undefined- 要求所有代码的变异分数达到100%
- 对所有代码运行变异测试(速度过慢)
- 忽略等价变异体
- 对getter/setter进行变异测试
- 对生成代码运行变异测试
- 跳过复杂逻辑的变异测试
- 仅关注代码行覆盖率
Examples
工具列表
See also: test-data-generation, continuous-testing, code-metrics-analysis for comprehensive test quality measurement.
- JavaScript/TypeScript:Stryker Mutator
- Java:PITest, Major
- Python:mutmut, Cosmic Ray
- C#:Stryker.NET
- Ruby:Mutant
- PHP:Infection
—
与CI集成
—
yaml
undefined—
.github/workflows/mutation-testing.yml
—
name: Mutation Testing
on:
pull_request:
paths:
- 'src/**'
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx stryker run
- name: Check mutation score
run: |
SCORE=$(jq '.mutationScore' stryker-reports/mutation-score.json)
if (( $(echo "$SCORE < 80" | bc -l) )); then
echo "Mutation score $SCORE% is below threshold"
exit 1
fi
undefined—
相关参考
—
另可参考:测试数据生成、持续测试、代码指标分析,以实现全面的测试质量评估。