Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kill the mutants, test your tests

Kill the mutants, test your tests

Joy of Coding

May 29, 2015
Tweet

More Decks by Joy of Coding

Other Decks in Programming

Transcript

  1. WHO DOES • Unit testing • Test-driven development (TDD) •

    Continuous integration • Measure code coverage • Mutation testing
  2. UNIT TESTING • Prove your code works • Instant regression

    tests • Improve code design • Has become a mainstream practise over the last 10 years
  3. CODE COVERAGE • Who has seen (or written?) tests •

    without verifications or assertions? • just to fake and boost coverage? • 100% branch coverage proves nothing
  4. MUTATION TESTING • Proposed by Richard J. Lipton in 1971

    (winner of 2014 Knuth Prize) • A better way to measure the quality of your tests • Surge of interest in the 1980s • Time to revive this interest!
  5. MUTATION TESTING • Generate (a lot of) mutants of your

    codebase • Run (some of) your unit tests • Check the outcome!
  6. OUTCOME #1: KILLED • A mutant is killed if a

    test fails (detecting the mutated code) • This proves the mutated code is properly tested
  7. OTHER OUTCOMES • NON-VIABLE • JVM could not load the

    mutant bytecode • MEMORY ERROR • JVM ran out of memory during test • RUN ERROR • An error but none of the above.
  8. FAULT INJECTION? • With fault injection you test code •

    Inject faults/mutations and see how the system reacts • With mutation testing you test your tests • Inject faults/mutations and see how the tests react
  9. TOOLING • µJava: http://cs.gmu.edu/~offutt/mujava/ (inactive) • Jester: http://jester.sourceforge.net/ (inactive) •

    Jumble: http://jumble.sourceforge.net/ (inactive) • javaLanche: http://www.st.cs.uni-saarland.de/mutation/ (inactive) • PIT: http://pitest.org/
  10. USING PIT • PIT uses configurable ‘mutators' • ASM (bytecode

    manipulation) is used to mutate your code • No mutated code is stored, it can't interfere with your code • Generates reports with test results
  11. MUTATORS: NEGATE CONDITIONALS == into !=
 != into ==
 <=

    into >
 >= into <
 < into >=
 > into <=
  12. MUTATORS: MATH + into -
 - into +
 * into

    /
 / into *
 % into *
 & into | << into >>
 >> into <<
 >>> into <<<
 a++ into a--
 a-- into a++
  13. MUTATORS: MANY MORE • Replacing return values (return a; becomes

    return 0;) • Removal of void invocations (doSomething(); is removed) • Some enabled by default, others are optional/configurable
  14. MUTATION TESTING IS SLOW? • Speed was unacceptable in the

    80's • Mutation testing is still CPU intensive • But PIT has a lot of methods to speed it up!
  15. WHICH TESTS TO RUN? • PIT uses code coverage to

    decide which tests to run: • A mutation is on a line covered by 3 tests? Only run those.
  16. SIMPLE EXAMPLE • 100 classes • 10 unit tests per

    class • 2 ms per unit test • Total time (all tests): 100 x 10 x 2ms = 2s
  17. SIMPLE EXAMPLE • Total time (all tests): 100 x 10

    x 2ms = 2s • 8 mutants per class, 100 classes x 8 = 800 mutants • Brute force: 800 x 2s = 26m40s • Smart testing: 800 x 10 x 2ms = 16s
  18. LONGER EXAMPLE • Total time (all tests): 1000 x 10

    x 2ms = 20s • 8 mutants per class, 1000 classes x 8 = 8000 mutants • Brute force: 8000 x 20s = 1d20h26m40s…!!! • Smart testing: 8000 x 10 x 2ms = 2m40s
  19. PERFORMANCE TIPS • Write fast tests • Good separation or

    concerns • Use small classes, keep amount of unit tests per class low
  20. INCREMENTAL ANALYSIS • Experimental feature • Incremental analysis keeps track

    of: • Changes in the codebase • Previous results
  21. HOW ABOUT MOCKING? • PIT has support for: • Mockito,

    EasyMock, JMock, PowerMock and JMockit
  22. HOW TO USE PIT? • Standalone Java process • Build:

    Ant task, Maven plugin • CI: Sonarqube plugin, Gradle plugin • IDE: Eclipse plugin (Pitclipse), IntelliJ Plugin
  23. STANDALONE JAVA java -cp <your classpath including pit jar and

    dependencies>
 org.pitest.mutationtest.commandline.MutationCoverageReport
 --reportDir /somePath/
 --targetClasses com.your.package.tobemutated*
 --targetTests com.your.package.*
 --sourceDirs /sourcePath/
  24. MAVEN PLUGIN Run as: mvn clean package org.pitest:pitest-maven:mutationCoverage <plugin> <groupId>org.pitest</groupId>

    <artifactId>pitest-maven</artifactId> <version>1.0.0</version> <configuration> <targetClasses> <param>com.your.package.tobemutated*</param> </targetClasses> <jvmArgs>…</jvmArgs> </configuration> </plugin>
  25. USE CASE The price of an item is 17 euro

    If you buy 20 or more, all items cost 15 euro If you have a coupon, all items cost 15 euro
  26. CODE public int getPrice(int amountOfThings, boolean coupon) { if (amountOfThings

    >= 20 || coupon) { return amountOfThings * 15; } return amountOfThings * 17; }
  27. TEST #1 @Test public void testNormalPricing() { //Not enough for

    discount: int amount = 1; Assert.assertEquals(17, businessLogic.getPrice(amount, false)); }
  28. BRANCH COVERAGE public int getPrice(int amountOfThings, boolean coupon) { if

    (amountOfThings >= 20 || coupon) { return amountOfThings * 15; } return amountOfThings * 17; }
  29. TEST #2 @Test public void testDiscountPricingByAmount() { //Enough for discount:

    int amount = 100; Assert.assertEquals(1500, businessLogic.getPrice(amount, false)); }
  30. BRANCH COVERAGE public int getPrice(int amountOfThings, boolean coupon) { if

    (amountOfThings >= 20 || coupon) { return amountOfThings * 15; } return amountOfThings * 17; }
  31. TEST #3 @Test public void testDiscountWithCoupon() { //Not enough for

    discount, but coupon: int amount = 1; Assert.assertEquals(15, businessLogic.getPrice(amount, true)); }
  32. BRANCH COVERAGE public int getPrice(int amountOfThings, boolean coupon) { if

    (amountOfThings >= 20 || coupon) { return amountOfThings * 15; } return amountOfThings * 17; }
  33. PIT RESULT > org.pitest.mutationtest…ConditionalsBoundaryMutator >> Generated 1 Killed 0 (0%)

    > KILLED 0 SURVIVED 1 TIMED_OUT 0 NON_VIABLE 0 > MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 > NO_COVERAGE 0 PIT tells us: Changing >= into > doesn’t trigger a failing test
  34. TEST #4 @Test public void testDiscountAmountCornerCase() { //Just enough for

    discount, mutation into > should fail this test int amount = 20; Assert.assertEquals(300, businessLogic.getPrice(amount, true)); }
  35. BRANCH COVERAGE public int getPrice(int amountOfThings, boolean coupon) { if

    (amountOfThings >= 20 || coupon) { return amountOfThings * 15; } return amountOfThings * 17; }
  36. PIT RESULT > org.pitest.mutationtest…ConditionalsBoundaryMutator >> Generated 1 Killed 0 (0%)

    > KILLED 0 SURVIVED 1 TIMED_OUT 0 NON_VIABLE 0 > MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 > NO_COVERAGE 0 STILL WRONG!?
  37. DID YOU SPOT THE BUG? @Test public void testDiscountAmountCornerCase() {

    //Just enough for discount, mutation into > should fail this test int amount = 20; Assert.assertEquals(300, businessLogic.getPrice(amount, true)); }
  38. SUMMARY • Mutation testing automatically tests your tests • Mutation

    testing can find bugs in your tests • Code coverage is wrong, gives a false sense of security • Mutation testing with PIT is easy to implement