EclipseCon Europe 2013: Take back Control over your Test Suite

EclipseCon Europe 2013: Take back Control over your Test Suite

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 for your Eclipse plugins. 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.

A5a5cadf1ec1a7781c109f9bc7b37028?s=128

Sebastian Benz

October 31, 2013
Tweet

Transcript

  1. 6.

    @Test public void generatesCode() { IProject project = workspace.getProject("MyProject"); project.create(...);

    CodeGenerator codeGenerator = new CodeGenerator(project); ! ! ! ! } Input someInput = ...; codeGenerator.generate(someInput); IFile generatedFile = project.getFile("MyFile.java"); assertTrue(generatedFile.exists()); Lack of focus
  2. 7.

    import org.eclipse.core.resources.IProject; ! public class CodeGenerator { private IProject project;

    ! public CodeGenerator(IProject project) { this.project = project; } ! public void generate(Input input) { ... IFile file = project.getFile(someFileName); file.create(someContent, true, monitor); } ! } Problem: Hard-wired dependency to Eclipse workspace
  3. 8.

    public class CodeGenerator { private FileSystem fileSystem; ! public CodeGenerator(FileSystem

    fileSystem) { this.fileSystem = fileSystem; } ! public void generate(Input input) { ... ! fileSystem.write(fileName, content); } ! } public interface FileSystem { ! void write(String fileName, String content); ! } simpler implementation
  4. 9.

    FileSystem fileSystem = new EclipseFileSystem("MyProject"); CodeGenerator codeGenerator = new CodeGenerator(fileSystem);

    codeGenerator.generate(someInput); Eclipse Command Line Test MockFileSystem fileSystem = new MockFileSystem(); CodeGenerator codeGenerator = new CodeGenerator(fileSystem); codeGenerator.generate(someInput); fileSystem.assertExists("SomeFile.java"); JavaIOFileSystem fileSystem = new JavaIOFileSystem("~/MyProject"); CodeGenerator codeGenerator = new CodeGenerator(fileSystem); codeGenerator.generate(someInput); this wasn’t possible before
  5. 14.

    @Test public void writesGeneratedFilesToFileSystem() { FileSystem fileSystem = mock(FileSystem.class); CodeGenerator

    codeGenerator = new CodeGenerator(fileSystem); Input someInput = ...; CodeGenerator.generate(someInput); ! verify(fileSystem).write("MyFile.java", "expected contents”); }
  6. 16.

    public abstract class FileSystemContractTest { @Test public void writesContentToFile() {

    FileSystem fileSystem = createFileSystem(); fileSystem.write("my_file.txt", "content"); assertEquals("content", contentsOf("my_file.txt")); } ! protected abstract FileSystem createFileSystem(); ! protected abstract String contentsOf(String file); ! } public class JavaIoFileSystemTest extends FileSystemContractTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); ! @Override protected String contentsOf(String return Files.toString(tempFolder.newFile(fileName), UTF_8); } ! @Override protected FileSystem createFileSystem() { return new JavaIOFileSystem(tempFolder.getRoot().getAbsolutePath()); } ! } public class JavaIoFileSystemContractTest extends FileSystemContractTest { ! @Override protected String contentsOf(String fileName) { return Files.toString(fileName)); } ! @Override protected FileSystem createFileSystem() { return new JavaIOFileSystem(...); } ! }
  7. 19.

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

    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()); } } Problem: ‘new’ couples implementation and test
  9. 22.

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

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

    underAgePerson = createPerson(17); assertFalse(underAgePerson.isAllowedToDrive()); } ! @Test public void adultsAreAllowedToDrive() { Person adultPerson = createPerson(18); assertTrue(adultPerson.isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { Person underAgePerson = createPerson(17); assertFalse(underAgePerson.isAllowedToDrink()); } ! ... ! private void createPerson(int age){ return new Person(age); } }
  11. 24.

    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 void anUnderAgePerson(){ return new Person(17); } ! private void anAdultPerson(){ return new Person(18); } ! private void createPerson(int age){ return new Person(age); } }
  12. 25.

    public class PersonTest { @Test public void underAgePersonsAreNotAllowedToDrive() { assertFalse(PersonBuilder.underAgePerson().build().isAllowedToDrive());

    } ! @Test public void adultsAreAllowedToDrive() { assertTrue(PersonBuilder.adultPerson().build().isAllowedToDrive()); } ! @Test public void underAgePersonsAreNotAllowedToDrink() { assertFalse(PersonBuilder.underAgePerson().build().isAllowedToDrink()); } ! }
  13. 26.

    public class CodeGenerationInWorkspaceTest { @Before public void cleanWorkspace(){ Workspaces.clean(); }

    ... ! ! } public class WorkspaceFileImportTest { @Before public void cleanWorkspace(){ Workspaces.clean(); } ... ! }
  14. 27.

    public class CodeGenerationInWorkspaceTest { @Before public void cleanWorkspace(){ Workspaces.clean(); Editors.closeAllOpen();

    } ... ! ! } public class WorkspaceFileImportTest { @Before public void cleanWorkspace(){ Workspaces.clean(); } ... ! }
  15. 29.

    public class WorkspaceRule implements TestRule { ! @Override public Statement

    apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { clearWorkspace(); base.evaluate(); } }; } private void clearWorkspace(){ Workspaces.clean(); Editors.closeAllOpen(); } ! } JUnit Rules FTW
  16. 30.

    public class WorkspaceRule implements TestRule { ! @Override public Statement

    apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { clearWorkspace(); base.evaluate(); } }; } private void clearWorkspace(){ Workspaces.clean(); Editors.closeAllOpen(); } ! } JUnit Rules FTW this runs the test
  17. 31.

    public class CodeGenerationInWorkspaceTest { @Rule public WorkspaceRule workspace = new

    WorkspaceRule(); ! ... ! } public class WorkspaceFileImportTest { @Rule public WorkspaceRule workspace = new WorkspaceRule(); ! ! ... ! } JUnit Rules FTW
  18. 33.

    How can I improve my design to make the test

    easier? When something is hard to test, ask yourself:
  19. 35.

    THREE WAYS TO TAKE BACK CONTROL #1 Focus your tests

    #2 Write clean tests #3 Listen to your tests
  20. 36.

    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 - 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 - Working Effectively with Legacy Code, Michael Feathers, http:// www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/ dp/0131177052