Take (back) Control over your Test Suite (I.T.A.K.E 2014)

Take (back) Control over your Test Suite (I.T.A.K.E 2014)

We all know testing is important. Unfortunately, even the best intentions are often crushed by slow and brittle tests hindering development more than they actually help.

But this must not necessarily be the case. In this talk I want to present some simple techniques helping you to write better tests. In particular, the following topics will be covered:

- how to write maintainable tests that don’t break on every change.
- how tests help you writing better code.
- how to speedup your test suite.
- how to avoid integration tests

http://2014.itakeunconf.com/sebastian-benz-take-back-control-over-your-test-suite/

A5a5cadf1ec1a7781c109f9bc7b37028?s=128

Sebastian Benz

May 29, 2014
Tweet

Transcript

  1. TAKE (BACK) CONTROL OVER YOUR TEST SUITE SEBASTIAN BENZ -

    @sebabenz E S R L A B S
  2. SLOW & FRAGILE

  3. #1 FOCUS YOUR TESTS

  4. FRAGILITY FEEDBACK vs

  5. - Specific feedback - Minimizes fragility FOCUSED TEST

  6. An example application

  7. input result

  8. input result Lack of focus: to many components involved

  9. input result Focused test on unit-level

  10. input result Focused test on application-level

  11. Should we create a test for every class?

  12. T

  13. T

  14. T Do we need to create tests for extracted classes?

  15. High test to code ratio indicates high coupling Light Bulb

    by Roy Verhaag from The Noun Project As long as extracted classes do to not contain new behaviour, more tests do not necessarily improve the feedback but increase fragility, as tests are more coupled to the implementation.
  16. ! EclipseFileSystem fileSystem = new EclipseFileSystem(); CodeGenerator codeGenerator = new

    CodeGenerator(fileSystem); ! ! Input someInput = ...; codeGenerator.generate(someInput); assertEquals("contents”, fileSystem.contensOf(“MyFile.java")); Problem: coupling to implementation details makes it hard to focus test
  17. public class CodeGenerator { private EclipseFileSystem fileSystem; ! public CodeGenerator(EclipseFileSystem

    fileSystem) { this.fileSystem = fileSystem; } ! public void generate(Input input) { ... fileSystem.writeFile(fileName, content); } ! }
  18. public class CodeGenerator { private FileSystem fileSystem; ! public CodeGenerator(FileSystem

    fileSystem) { this.fileSystem = fileSystem; } ! public void generate(Input input) { ... fileSystem.writeFile(fileName, content); } ! } public interface FileSystem { ! void writeFile(String fileName, String content); ! void contentsOf(String fileName,); ! }
  19. FileSystem fileSystem = new InMemoryFileSystem(); CodeGenerator codeGenerator = new CodeGenerator(fileSystem);

    codeGenerator.generate(someInput); assertEquals("contents”, fileSystem.contensOf(“MyFile.java")); Faster, more focused test based on abstraction
  20. EclipseFileSystem CodeGenerator

  21. FileSystem CodeGenerator EclipseFileSystem Dependency Inversion

  22. FileSystem CodeGenerator EclipseFileSystem Parser Converter Integration Test? …and here? …and

    here?
  23. FileSystem CodeGenerator EclipseFileSystem Interaction Test

  24. FileSystem fileSystem = mock(FileSystem.class); CodeGenerator codeGenerator = new CodeGenerator(fileSystem); Input

    someInput = ...; codeGenerator.generate(someInput); verify(fileSystem).writeFile("MyFile.java", "expected contents");
  25. Interaction Test FileSystem CodeGenerator EclipseFileSystem Contract Test Use interaction +

    contract tests to avoid integration tests
  26. public abstract class FileSystemContractTest { @Test public void writesContentToFile() {

    FileSystem fileSystem = createFileSystem(); fileSystem.writeFile("my_file.txt", "content"); assertEquals("content", contentsOf("my_file.txt")); } ! protected abstract FileSystem createFileSystem(); ! } public class EclipseFileSystemContractTest extends FileSystemContractTest { ! @Override protected FileSystem createFileSystem() { return new EclipseFileSystem(...); } ! }
  27. FEEDBACK > FRAGILITY Before writing a test, ask yourself: “Does

    the feedback by the test justify the potential fragility?”
  28. and

  29. It is OK to delete tests!

  30. #2 WRITE CLEAN TESTS

  31. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { Person

    underAgePerson = new Person(17); assertFalse(underAgePerson.isAllowedToDrive()); } ! @Test public void adultsAreAllowedToDrive() { Person adultPerson = new Person(18); assertTrue(adultPerson.isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { Person underAgePerson = new Person(17); assertFalse(underAgePerson.isAllowedToDrink()); } ! @Test public void adultsAreAllowedToDrink() { Person adultPerson = new Person(18); assertTrue(adultPerson.isAllowedToDrink()); } }
  32. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { Person

    underAgePerson = new Person(17); assertFalse(underAgePerson.isAllowedToDrive()); } ! @Test public void adultsAreAllowedToDrive() { Person adultPerson = new Person(18); assertTrue(adultPerson.isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { Person underAgePerson = new Person(17); assertFalse(underAgePerson.isAllowedToDrink()); } ! @Test public void adultsAreAllowedToDrink() { Person adultPerson = new Person(18); assertTrue(adultPerson.isAllowedToDrink()); } }
  33. Avoid duplicated coupling between test and implementation.

  34. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { Person

    underAgePerson = aPersonWithAge(17); assertFalse(underAgePerson.isAllowedToDrive()); } ! @Test public void adultsAreAllowedToDrive() { Person adultPerson = aPersonWithAge(18); assertTrue(adultPerson.isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { Person underAgePerson = aPersonWithAge(17); assertFalse(underAgePerson.isAllowedToDrink()); } ! ... ! private Person aPersonWithAge(int age){ return new Person("any name", age); } }
  35. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { assertFalse(anUnderAgePerson().isAllowedToDrive());

    } ! @Test public void adultsAreAllowedToDrive() { assertTrue(anAdultPerson().isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { assertFalse(anUnderAgePerson().isAllowedToDrink()); } ! private Person anAdultPerson(){ return aPersonWithAge(18); } ! private Person anUnderAgePerson(){ return aPersonWithAge(17); } ! private Person aPersonWithAge(int age){ return new Person("any name", age); } }
  36. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { assertFalse(anUnderAgePerson().isAllowedToDrive());

    } ! @Test public void adultsAreAllowedToDrive() { assertTrue(anAdultPerson().isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { assertFalse(anUnderAgePerson().isAllowedToDrink()); } ! private Person anAdultPerson(){ return aPersonWithAge(Person.FULL_AGE); } ! private Person anUnderAgePerson(){ return aPersonWithAge(Person.FULL_AGE - 1); } ! private Person aPersonWithAge(int age){ return new Person("any name", age); } } Beware of hidden duplicated coupling
  37. public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { assertFalse(Persons.anUnderAgePerson().isAllowedToDrive());

    } ! @Test public void adultsAreAllowedToDrive() { assertTrue(Persons.anAdultPerson().isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { assertFalse(Persons.anUnderAgePerson().isAllowedToDrink()); } ! }
  38. When tests break, remove any duplicated coupling between test and

    implementation.
  39. public class CodeGenerationInWorkspaceTest { @Before public void setup(){ Workspaces.clean(); }

    ... ! ! } public class WorkspaceFileImportTest { @Before public void setup(){ Workspaces.clean(); } ... ! }
  40. public class CodeGenerationInWorkspaceTest { @Before public void setup(){ Editors.closeAllOpen(); Workspaces.clean();

    } ... ! ! } public class WorkspaceFileImportTest { @Before public void setup(){ Workspaces.clean(); } ... ! }
  41. Encapsulate test setups

  42. public class WorkspaceRule implements TestRule { ! @Override public Statement

    apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { Workspaces.clean(); Editors.closeAllOpen(); base.evaluate(); } }; } ! } JUnit Rules FTW
  43. public class CodeGenerationInWorkspaceTest { @Rule public WorkspaceRule workspace = new

    WorkspaceRule(); ! ... ! } public class WorkspaceFileImportTest { @Rule public WorkspaceRule workspace = new WorkspaceRule(); ! ! ... ! } JUnit Rules FTW Favour composition over inheritance
  44. #3 LISTEN TO YOUR TESTS

  45. HARD TO TEST FUNCTIONALITY IN ISOLATION Sign of too many

    responsibilities in your class
  46. EXCESSIVE TEST SETUPS Too much coupling

  47. MOCKING HELL Potential violation of Law of Demeter

  48. SLOW TESTS Potential missing abstractions

  49. HARD TO TEST CONCURRENCY Avoid mixing concurrency and business logic

  50. State leaks through tests Singletons are a code smell

  51. Take testing pains as a chance to ask: “How can

    I improve my design to make testing easier?”
  52. THREE WAYS TO TAKE BACK CONTROL #1 Focus your tests

    #2 Write clean tests #3 Listen to your tests
  53. @sebabenz E S R L A B S

  54. Recommended Reading -Growing Object-Oriented Systems guided by tests, Nat Pryce

    & Steve Freeman, http://www.growing-object-oriented-software.com -XUnit Test Patterns, Gerard Meszaros, http://xunitpatterns.com -Michael Feathers - the deep synergy between testability and good design https://www.youtube.com/watch?v=4cVZvoFGJTU&feature=kp -Integration Tests are Scam, J. B. Rainsberger, http://www.infoq.com/ presentations/integration-tests-scam -Subcutaneus Tests, Martin Fowler, http://martinfowler.com/bliki/ SubcutaneousTest.html -Duplicated Test Code & High Coupling, Jason Gorman, http:// codemanship.co.uk/parlezuml/blog/?postid=1182