Mutation Testing
Mutation analysis, mutation testing
Mutation
A mutation is a small modification to a piece of code
- The term is inspired by "biological mutation"
- done by applying mutation operators
A mutation operator is a rule that specifies a valid syntactic modification
A mutant is the modified (i.e., mutated) code after applying a mutation operator.
Example:
// original code
int index = 0; while(true) {
index++;
// control flow branching
if (index == 10)
break;
}
// mutated code
int index = 0;
while (true) {
index++;
// mutating
if (index <= 10)
break;
}
Mutation Testing
- Mutating a piece of code to generate a set of mutants
- "Kill" all the (or as many as possible) mutants.
Mutation Testing: Assume is the set of all mutants for a given piece of code . Test is said to "kill" if and only if the output of on is different from the output of on .
Example:
// Program P
int min (int a, int b) {
if (a < b) {
return a;
}
return b;
}
// Mutant m
int min (int a, int b) {
if (a > b) {
return a;
}
return b;
}
- In test t1:
a = 10,b = 12kills m - In test t2:
a = 10,b = 10does not kill m
The number of test requirements for mutation depends on two things:
- the syntax of the artifact being mutated
- the mutation operators
Mutation testing is difficult to apply by hand, is effective ("gold standard" of testing), and is often used to evaluate other crtierias.
Help the test engineer to strengthen the quality of the tests
- At least one test case in the test suite to kill mutant
Designing effective mutation operators is the key.
Mutation Operators are designed to:
- mimic typical programmer mistakes
- encourage the test engineer to write fault-revealing tests
Coupling effective. The mutation testing premise is that:
"Test data that distinguishes all programs differing from a correct one by only simple errors is so sensitive that it also implicitly distinguishes more complex errors.”
If mutation operators are designed well, the resulting tests will be very powerful
Different operators must be defined for different programming languages and different goals.
Testers can keep adding tests until all the mutanta have been killed. Some terms:
- Dead mutant: a test case has killed it
- Stillborn mutant: syntatically illegal
- Trivial mutant: almost every test can kill it
- Equivalent mutant: no test case can kill it (same behavior as original)
- This is a mutant that is functionally equivalent to the orignal code. Thus, no test case can kill an equivalent mutant -> infeasible test requirement
- In general, an undecidable problem to identify an equivalent mutant.
Example for equivalent mutant:
// # original code
int index = 0;
while(true) {
index++;
// control flow branching
if (index == 10)
break;
}
// # an equivalent mutant
int index = 0;
while (true) {
index++;
// mutating
if (index >= 10)
break;
}
RIPR Model
The RIPR model discussed earlier in class:
- Reachability: the test causes the faulty statement to be reached (in mutation - the mutated statement)
- Infection: the test causes the faulty statement to result in an incorrect state
- Propagation: the incorrect state propagates to incorrect output
- Revealability: the tester must observe part of the incorrect output.
This leads to two variants of muation coverage.
Mutation Coverage
Mutation Coverage (MC): for each , contains exactly one requirement, to kill .
Coverage in mutation equates to number of mutants killed MC level is also called mutation score, which is: (number of mutants killed)/(total number of mutants).
Mutation Operator Coverage (MOC): for each mutant operator, contains exactly one requirement, to create a mutant that is derived using the mutant operator.
RIPR model has two variants of mutation coverage
- Strongly killing mutants: given a mutant for a program and a test , is said to strongly kill if and only if the output of on is different from the output of on .
- Weakly killling mutants: given a mutant that modifies a location in a program , and a test , is said to weakly kill if and only if the state of the execution of on is different from the state of the execution of on immediately after .
- Weakly killing satisfies reachability and infection, but not propagation.
Strong Mutation Coverage (SMC): for each , contains one requirement: to strongly kill .
Weak Mutation Coverage (WMC): for each , contains exactly one requirement, to weakly kill .
- Named "weak mutation" because it is easier to kill mutants under this assumption (also requires less analysis)
- A few mutants can be killed under weak mutation but not under strong mutation (no propagation)
"studies have found that test sets that weakly kill all mutants also strongly kills most mutants".
Compare these two:
int gcd (int x, int y) {
int tmp;
while (y != 0) {
tmp = x % y; // weak mutation
x = y;
y = tmp;
}
return x; // strong mutation here
}
int gcd (int x, int y) {
int tmp;
while (y != 0) {
tmp = x * y; // weak mutation (operator!)
x = y;
y = tmp;
}
return x; // strong mutation here
}
Mutation Operators
Some mutation operators for java:
ABS: Absolute Value Insertion
- each arithmetic expression (and subexpression) is modified by the functions abs(), negAbs(), and failOnZero().
a = m * (o + p);
a = abs(m * (o + p)); // ∆1
a = m * abs ((o + p)); // ∆2
a = failOnZerl(m * (o + 0)); // ∆3
ROR: Relational Order Replacement
- each occurence of the relational operators is replaced by each of the other operators by
falseOpandtrueOp
if (x <= y)
if (x > y) // ∆1
if (x < y) // ∆2
if (x falseOp y) // ∆3, always returns false
LOR: Logical Operator Replacement
- each occurence of the logical operators is replaced by each of the other operators. In addition, each is replaced by
falseOp,trueOp,leftOp,rightOp.
if (x <= y && a > 0)
if (x <= y || a > 0) // ∆1
if (x <= y leftOp a > 0) // ∆2. returns result of left clause
SOR: shift operator replacement
- each occurence of one of the shift operators is replaced by each of the other operators. In addition, each is replaced by the special mutation operator
leftOp.
byte b = (byte) 16;
b = b >> 2;
b = b << 2; // ∆1
b = b leftOp 2; // ∆2, result is b
BOR: Bitwise Operator Replacement
- Each occurrence of one of the bitwise operators is replaced by each of the other operators. In addition, each is replaced by
leftOpandrightOp.
int a = 60;
int b = 13;
int c = a & b;
int c = a | b; // ∆1
int c = a rightOp b; // ∆2, result is b
ASR: Assignment Operator Replacement
- each occurence of one of the assignment operators is replaced by each of the other operators
a = m * (o + p)
a += m * (o + p); // ∆1
a *= m * (o + p); // ∆2
UOI: Unary Operator Insertion:
- each unary operator is inserted in front of each espression of the correct type
a = m * (o + p);
a = m * -(o + p); // ∆1
a = -(m * (o + p)); // ∆2
UOD: Unary Operator Deletion
- each unary operator is deleted
if !(X <= Y && !Z)
if (X > Y && !Z) // ∆1
if !(X < Y && Z) // ∆2
SVR: Scalar Variable Replacement
- Each variable reference is replaced by every other variable of the appropriate type that is declared in the current scope.
a = m * (o + p);
a = o * (o + p); // ∆1
a = m * (m + p); // ∆2
a = m * (o + o); // ∆3
p = m * (o + p); // ∆4
BSR: Bomb Statement Replacement
- Each statement is replaced by a special Bomb() function.
a = m * (o + p);
Bomb() // ∆1, Raises exception when reached
Ways to Improve Mutation Testing Efficiency
Should more than one mutation operator be applied at the same time to produce a mutant?
Should we try each and every possible mutation operator on any program
To improve:
- parallelize
- mutate bytecode instead of source code (no need to recompile)
- selective mutation
- only a subset of mutation operators
- strong vs weak killing
- use weak killing
Mutation testing is expensive (lots of mutants):
- sample from the set of all mutants
- selectively use mutation operators that are (more) effective.
If tests that are created to kill mutants produced by mutation operator also kill mutants produced by mutation operator , we say that is more effective than .
Effective Mutation Operators: if tests that are created specifically to kill mutant created by a collection of mutation operators also kill mutants created by all remaining mutation operators with very high probability, then defines an effective set of mutation operators.
Researchers have concluded that the collection of operators that insert unary operators and that modify unary and binary operators are effective.
Integration Mutation Testing
Integration Testing: Testing multiple units together. Specifically, testing connections among units.
- In Java, testing the way classes, packages, and modules are connected. For example, which methods of class A calls which methods of class B and vice versa.
Integration testing is often based on the existing couplings – the explicit and implicit relationships among software components
Faults related to integrations often depend on a mismatch of assumptions
- Callee thought a list was sorted, caller did not
- Callee thought all fields were initialized, caller only initialized some of the fields
- Caller sent values in kilometers, callee thought they were miles
- etc.
Integration Mutation Operators: In general, there are four kinds:
- change a calling method by modifying values that are sent to a called method
- change a calling method by modifying the call change a called method by modifying values that enter and leave a method
- includes parameters as well as variables from higher scopes (class level, package, public, etc.)
- change a called method by modifying return statements from the method
IPVR: Integration Parameter Variable Replacement
- Each parameter in a method call is replaced by each other variable in the scope of the method call that is of compatible type
int a, b;
callMethod(a);
callMethod(b); // ∆
IPEX: Integration Parameter Exchange
- Each parameter in a method call is exchanged with each parameter of compatible types in that method call
max (a, b);
max (b, a); // ∆
IUOI: Integration Unary Operator insertion
- Each expression in a method call is modified by inserting all possible unary operators in front and behind it
callMethod (a);
callMethod (a++); // ∆
callMethod (++a); // ∆
callMethod (a--); // ∆
callMethod (--a); // ∆
IMCD: Integration Method Call Deletion
- Each method call is deleted. If the method returns a value and it is used in an expression, the method call is replaced with an appropriate constant value
Max (a, b);
X = new Integer (0); // ∆
IREM: Integration Return Expression Modification Each expression in each return statement in a method is modified by applying the UOI and AOR operators
int myMethod () {
return a + b;
return ++a + b; // ∆
return a – b; // ∆
}
Object-Oriented Mutation
eSo far, we only mutated method bodies. Class level mutation operators can be utilized too.
public class test {
// ...
protected void do() {
// ...
}
}
public class test {
// ...
public void do() { // emphasis on public!
// ...
}
}
There's a lot of objected-oriented mutation operators.
Summary
Mutation testing is
- widely considered the strongest test criterion
- a high-end type of testing i.e., more effective, also more expensive, i.e., by far the most test requirements
- It's expensive to: generate mutants + recompilations + running all test cases of the test suite on each generated mutant
- Mutation Analysis is assessing the quality of a test suite
- Mutation Testing is used to improve the test suite quality by leveraging mutation analysis
- Can be applied to any syntax, hence aka syntax-based testing:
- have been applied to formal specification languages such as SVM
- have been applied to markup languages such as XML, HTML etc
Tools to use:
- Java : muJava, PIT, Javalanche, etc.
- Javascript : Stryker Mutator, etc.
- C++: MuCPP, Mutate++, etc.
- Python: Cosmic Ray, mutmut, etc.
- Ruby: Heckle
- PHP: Infection PHP, Humbug
- C#: Nester, VisualMutator
Linked from Software Testing and Debugging