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

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/

Sebastian Benz

May 29, 2014
Tweet

More Decks by Sebastian Benz

Other Decks in Technology

Transcript

  1. TAKE (BACK)
    CONTROL OVER
    YOUR TEST
    SUITE
    SEBASTIAN BENZ - @sebabenz
    E S R
    L A B S

    View Slide

  2. SLOW & FRAGILE

    View Slide

  3. #1 FOCUS YOUR TESTS

    View Slide

  4. FRAGILITY
    FEEDBACK vs

    View Slide

  5. - Specific feedback
    - Minimizes fragility
    FOCUSED TEST

    View Slide

  6. An example application

    View Slide

  7. input
    result

    View Slide

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

    View Slide

  9. input result
    Focused test on unit-level

    View Slide

  10. input
    result
    Focused test on application-level

    View Slide

  11. Should we create a test
    for every class?

    View Slide

  12. T

    View Slide

  13. T

    View Slide

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

    View Slide

  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.

    View Slide

  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

    View Slide

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

    View Slide

  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,);
    !
    }

    View Slide

  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

    View Slide

  20. EclipseFileSystem
    CodeGenerator

    View Slide

  21. FileSystem
    CodeGenerator
    EclipseFileSystem
    Dependency Inversion

    View Slide

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

    View Slide

  23. FileSystem
    CodeGenerator
    EclipseFileSystem
    Interaction Test

    View Slide

  24. FileSystem fileSystem = mock(FileSystem.class);
    CodeGenerator codeGenerator = new CodeGenerator(fileSystem);
    Input someInput = ...;
    codeGenerator.generate(someInput);
    verify(fileSystem).writeFile("MyFile.java", "expected contents");

    View Slide

  25. Interaction Test
    FileSystem
    CodeGenerator
    EclipseFileSystem
    Contract Test
    Use interaction + contract tests
    to avoid integration tests

    View Slide

  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(...);
    }
    !
    }

    View Slide

  27. FEEDBACK > FRAGILITY
    Before writing a test, ask yourself: “Does the
    feedback by the test justify the potential fragility?”

    View Slide

  28. and

    View Slide

  29. It is OK to delete tests!

    View Slide

  30. #2 WRITE CLEAN TESTS

    View Slide

  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());
    }
    }

    View Slide

  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());
    }
    }

    View Slide

  33. Avoid duplicated coupling between
    test and implementation.

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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

    View Slide

  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());
    }
    !
    }

    View Slide

  38. When tests break, remove any duplicated
    coupling between test and implementation.

    View Slide

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

    View Slide

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

    View Slide

  41. Encapsulate test setups

    View Slide

  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

    View Slide

  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

    View Slide

  44. #3 LISTEN TO YOUR TESTS

    View Slide

  45. HARD TO TEST FUNCTIONALITY
    IN ISOLATION
    Sign of too many
    responsibilities in your class

    View Slide

  46. EXCESSIVE TEST SETUPS
    Too much coupling

    View Slide

  47. MOCKING HELL
    Potential violation of
    Law of Demeter

    View Slide

  48. SLOW TESTS
    Potential missing
    abstractions

    View Slide

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

    View Slide

  50. State leaks through tests
    Singletons are a code smell

    View Slide

  51. Take testing pains as a chance to ask:
    “How can I improve my design to make
    testing easier?”

    View Slide

  52. THREE WAYS TO TAKE BACK CONTROL
    #1 Focus your tests
    #2 Write clean tests
    #3 Listen to your tests

    View Slide

  53. @sebabenz
    E S R
    L A B S

    View Slide

  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

    View Slide