public class NodeMapperTest { @Test public void testGetMachineById() { // The boss says we should write tests. // Well, here you are. He-he. Assert.assertTrue(true); } }
$ mvn clean -Dtest=NodeMapperTest#testGetMachineById test jacoco:report
public class NodeMapper { //... public String getMachineById(long nodeId) { if(nodeId2machine.containsKey(nodeId)) { return nodeId2machine.get(nodeId); } else { return DEFAULT_MACHINE_NAME; } } }
private static final Map<String, List<Long>> MACHINES = /* ... */ @Test public void testGetMachineById2() { NodeMapper nodeMapper = new NodeMapper(MACHINES); nodeMapper.getMachineById(0); nodeMapper.getMachineById(1); }
$ mvn clean -Dtest=NodeMapperTest#testGetMachineById2 test jacoco:report
public class NodeMapper { //... public String getMachineById(long nodeId) { if(nodeId2machine.containsKey(nodeId)) { return nodeId2machine.get(nodeId); } else { return DEFAULT_MACHINE_NAME; } } }
System | Ant | Maven | Gradle | Java 8 | TestNG | *Mock* | Last Updated |
Javalanche | NO | NO | NO | NO | NO | ? | 2011 |
Jumble | YES | NO | NO | NO | NO | 3/6 | 2013 |
Jester | NO | NO | NO | NO | NO | ? | 2005 |
Simple Jester | NO | NO | NO | NO | NO | ? | 2009 |
PIT | YES | YES | YES | YES | YES | 6/6 | Yesterday |
$ mvn clean -Dtest=NodeMapperTest#testGetMachineById2 test pitest:mutationCoverage
7 33 34 35 36 37 38 39 40 41 |
public class NodeMapper { //... public String getMachineById(long nodeId) { if(nodeId2machine.containsKey(nodeId)) { return nodeId2machine.get(nodeId); } else { return DEFAULT_MACHINE_NAME; } } } |
public String getMachineById(long nodeId) {
if(!nodeId2machine.containsKey(nodeId)) {
return nodeId2machine.get(nodeId);
} else {
return DEFAULT_MACHINE_NAME;
}
}
public String getMachineById(long nodeId) {
if(nodeId2machine.containsKey(nodeId)) {
if(nodeId2machine.get(nodeId) != null) {
return null;
} else {
throw new RuntimeException();
}
} else {
return DEFAULT_MACHINE_NAME;
}
}
public String getMachineById(long nodeId) {
if(nodeId2machine.containsKey(nodeId)) {
return nodeId2machine.get(nodeId);
} else {
if(DEFAULT_MACHINE_NAME != null) {
return null;
} else {
throw new RuntimeException();
}
}
}
@Test public void testGetMachineById3() { NodeMapper nodeMapper = new NodeMapper(MACHINES); Assert.assertEquals(NodeMapper.DEFAULT_MACHINE_NAME, nodeMapper.getMachineById(0)); Assert.assertEquals(TEST_MACHINE_NAME, nodeMapper.getMachineById(1)); }
$ mvn clean -Dtest=NodeMapperTest#testGetMachineById3 test pitest:mutationCoverage
7 33 34 35 36 37 38 39 40 41 |
public class NodeMapper { //... public String getMachineById(long nodeId) { if(nodeId2machine.containsKey(nodeId)) { return nodeId2machine.get(nodeId); } else { return DEFAULT_MACHINE_NAME; } } } |
7 14 26 27 33 34 35 36 37 38 39 40 41 |
public class NodeMapper { public NodeMapper(Map<~> machines, String defaultMachineName) { //... this.defaultMachineName = defaultMachineName; } public String getMachineById(long nodeId) { if(nodeId2machine.containsKey(nodeId)) { return nodeId2machine.get(nodeId); } else { return DEFAULT_MACHINE_NAME; // <-- oops! } } } |
Changes the inequalities checks from inclusive to non-inclusive and vice versa
Before: if (a < b) { // do something } |
After: if (a <= b) {
// do something
}
|
Negates Conditionals
Before: if (foo()) { // do something } |
After: if (!foo()) {
// do something
}
|
Replaces binary operations by their counterparts
Before: int foo = bar + baz; |
After: int foo = bar - baz;
|
Also mutates field increments/decrements
class Foo { private int a; //... void bar() { a++; } } |
→ |
0: aload_0
1: dup
2: getfield
5: iconst_1
6: iadd # ← addition
7: putfield
10: return
|
Swaps increments to decrements and vice versa
Before: counter ++; |
After: counter --;
|
NB: only works on local variables
Removes the unary minus operator
Before: int a = -b; |
After: int a = b;
|
Changes the value returned by a method
Before: public int foo() { //... return true; } |
After: public int foo() {
//...
return false;
}
|
Removes a call to a void method
Before: public void foo() {/* ... */ } public void bar() { //... foo(); } |
After: public void foo() {/* ... */ }
public void bar() {
//...
}
|
Changes literal values assigned to locals
Before: public int foo() { int answer = 42; } |
After: public int foo() {
int answer = 43;
}
|
Could spawn lots of RuntimeExceptions
Removes a method call, replaces by assigning the default value
Before: public String foo() {/* ... */ } public void bar() { //... String foo = foo(); } |
After: public String foo() {/* ... */ }
public void bar() {
//...
String foo = null;
}
|
Could spawn lots of NullPointerExceptions and equivalent mutations
Removes a constructor call, to be replaced by assignment of null
Before: public void bar() { Object foo = new Object(); } |
After: public void bar() {
Object foo = null;
}
|
Spawns lots of NullPointerException
Here to replace INLINE_CONSTS, has more consistent behavior
Before: short s = Byte.MAX_VALUE; |
After: short s = Byte.MIN_VALUE;
|
Replaces values asigned to fields by default values
Before: public class Foo { private int magic = 13; //... } |
After: public class Foo {
private int magic = 0;
//...
}
|
Could generate lots of equivalent mutations
Before: int a; //... switch(a) { case 1: doFirst(); break; case 2: doSecond(); break; default: doDefault(); break; } |
After: int a; //... switch(a) { case 1: doDefault(); break; case 2: doDefault(); break; default: doFirst(); break; } |
You do not write any production code
Unless it is to make a failing unit test pass
You do not write any more of a unit test than is sufficient to fail
And compilation failures are failures
You do not write any production code
More than is sufficient to pass the one failing unit test
A success story from TheLadders.com[4]
Let's take an allegedly well-covered library and give it a go
$ ./prepare-commons-math3.sh && cd commons-math3 + svn checkout http://svn.apache.org/repos/asf/commons/proper/math/trunk@1455261 commons-math3 A commons-math3/RELEASE-NOTES.txt A commons-math3/... U commons-math3 Checked out revision 1455261. + patch commons-math3/pom.xml commons-math3-pom-patch.diff patching file commons-math3/pom.xml
$ mvn clean test jacoco:report ... Results : Tests run: 4901, Failures: 0, Errors: 0, Skipped: 43 [INFO] [INFO] --- jacoco-maven-plugin:0.6.2.201302030002:report (default-cli) @ commons-math3 --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 7:46.820s
The total Code Coverage is impressive: 92%
$ mvn clean test pitest:mutationCoverage ... ================================================================================ - Timings ================================================================================ > scan classpath : < 1 second > coverage and dependency analysis : 27 minutes and 31 seconds > build mutation tests : 7 seconds > run mutation analysis : 3 hours, 47 minutes and 7 seconds -------------------------------------------------------------------------------- > Total : 4 hours, 14 minutes and 46 seconds
The total Mutation Coverage is 79%
$ mvn clean test pitest:mutationCoverage -Dpit.threads=2 ... ================================================================================ - Timings ================================================================================ > scan classpath : < 1 second > coverage and dependency analysis : 27 minutes and 28 seconds > build mutation tests : 3 seconds > run mutation analysis : 2 hours, 2 minutes and 1 seconds -------------------------------------------------------------------------------- > Total : 2 hours, 29 minutes and 33 seconds
The improvement is nearly linear
PIT only considers the changed made to superclasses or outer classes (if any)
It does not take into account ny changes made to dependencies
$ mvn clean test pitest:mutationCoverage -P pit-history -Dpit.threads=4 ... ================================================================================ - Timings ================================================================================ > scan classpath : < 1 second > coverage and dependency analysis : 7 minutes and 15 seconds > build mutation tests : 2 seconds > run mutation analysis : 55 minutes and 41 seconds -------------------------------------------------------------------------------- > Total : 1 hours, 2 minutes and 59 seconds
Meta-info is saved nearly for free
193 194 195 196 197 198 199 200 201 202 203 204 |
// In org.apache.commons.math3.linear.MatrixUtils public static <T extends FieldElement<T>> FieldMatrix<T> createFieldIdentityMatrix(final Field<T> field, final int dimension) { final T zero = field.getZero(); final T one = field.getOne(); final T[][] d = MathArrays.buildArray(field, dimension, dimension); for (int row = 0; row < dimension; row++) { final T[] dRow = d[row]; Arrays.fill(dRow, zero); dRow[row] = one; } return new Array2DRowFieldMatrix<T>(field, d, false); } |
$ mvn clean test pitest:mutationCoverage -P pit-history -Dpit.threads=24 ... ================================================================================ - Timings ================================================================================ > scan classpath : < 1 second > coverage and dependency analysis : 7 minutes and 3 seconds > build mutation tests : 1 minutes and 1 seconds > run mutation analysis : 56 seconds -------------------------------------------------------------------------------- > Total : 9 minutes and 0 seconds --------------------------------------------------------------------------------
Joda Time ‐ an awesome library that lets you work with dates and times
public int evaluate(final int distance) { if(distance == 0) { throw new IllegalArgumentException("Distance should be non-zero"); } int result = 0; if(distance > 0) { result += calculateDistanceAdjustment(distance); } return result; }
5 6 7 8 9 10 11 12 13 14 15 16 17 |
public int evaluate(final int distance) { if(distance == 0) { throw new IllegalArgumentException("Distance should be non-zero"); } int result = 0; if(distance > 0) { result += calculateDistanceAdjustment(distance); } return result; } |
public static void rectify(String path) { ensurePathIsSecure(path); rmRf(path); } private static final void rmRf(String path) { System.out.println("$ rm -rf " + path); // Actually remove the directory }
stderr : PIT >> Running mutation [...] mutator=VoidMethodCallMutator, description=removed call to Minitrue::ensurePathIsSecure testsInOrder=[MinitrueTest.testRectifySecurity [...]] stderr : PIT >> mutating method rectify stderr : PIT >> 2 relevant test for rectify stderr : PIT >> replaced class with mutant in 2 ms stderr : PIT >> Running 1 units stdout : $ rm -rf /
$ git clone git@github.com:gvsmirnov/docs.git $ cd docs/presentations/java-mutation-testing
http://gvsmirnov.ru/docs/presentations/java-mutation-testing/
Is sure to work in Webkit-based browsers on Mac OS, may have issues on other browsers/platforms