Multi-purpose tests (Cool tricks with JUnit) JavaZone 2012 Johannes Brodwall, Principal Architect Steria
Part I
Properties of good test suites
Fast feedback Reasonable confidence in seconds
While you type! Holy crap! Get Infinitest!
Good coverage Never mind measured code coverage Do you catch many potential mistakes?
Robust Does refactoring break you tests? Do tests interfere with each other?
@Test public void should_filter_people() { Person person = Person.withName("Darth Vader"); Person nonMatching = Person.withName("Anakin Skywalker"); personRepository.savePerson(person); personRepository.savePerson(nonMatching); assertThat(personRepository.findPeople("vade")).contains(person).excludes(nonMatching); } While you type! Holy crap! Get Infinitest! “Act” and “assert” at same level of abstraction Allows for garbage in repository
Good test suites Fast feedback – ok confidence in seconds Coverage – breaks when they should Robust – don’t break when they shouldn’t
“Integration test” Sacrifice feedback for coverage
Part II
The configurable test
Starting point
public class PersonServletTest { private PersonServlet servlet = new PersonServlet(); private HttpServletRequest req = mock(HttpServletReq.class); private HttpServletResponse resp = mock(HttpServletResp.class); private PersonRepository personRepository = public void should_save_person() throws IOException { when(req.getParameter("full_name")).thenReturn("Darth Vader"); servlet.doPost(req, resp); verify(personRepository).savePerson(Person.withName("Darth Vader")); }
Fake instead of mock
public class PersonServletTest { private PersonServlet servlet = new PersonServlet(); private HttpServletRequest req = mock(HttpServletReq.class); private HttpServletResponse resp = mock(HttpServletResp.class); private PersonRepository personRepository = new public void should_save_person() throws IOException { when(req.getParameter("full_name")).thenReturn("Darth Vader"); servlet.doPost(req, resp); assertThat(personRepository.findAll()).contains(Person.withName("Darth Vader")); }
But we want also to use the real thing
public class PersonServlet WithRealRepo Test { private PersonServlet servlet = new PersonServlet(); private HttpServletRequest req = mock(HttpServletReq.class); private HttpServletResponse resp = mock(HttpServletResp.class); private PersonRepository personRepository = new public void should_save_person() throws IOException { when(req.getParameter("full_name")).thenReturn("Darth Vader"); servlet.doPost(req, resp); assertThat(personRepository.findAll()).contains(Person.withName("Darth Vader")); }
What if one test could do both?
@RunWith(RepositoryTestRunner.class) public class PersonServletTest { private PersonServlet servlet = new PersonServlet(); private HttpServletRequest req = mock(HttpServletReq.class); private HttpServletResponse resp = mock(HttpServletResp.class); private PersonRepository personRepository; public PersonServletTest(PersonRepository personRepo) { this.personRepository = personRepo; public void should_save_person() throws IOException { … }
I learned to write my own test runner by looking at org.junit.runner.Parameterized
The runner
public class RepositoryTestRunner extends Suite { private static JdbcConnectionPool dataSource = …; public RepositoryTestRunner(Class testClass) { super(testClass, createRunners(testClass)); } private static List createRunners(Class testClass) { List runners = new ArrayList<>(); runners.add(new RepoTestRunner(new FakePersonRepository(), testClass)); runners.add(new RepoTestRunner(new JdbcPersonRepository(dataSource), testClass)); return runners; } public static class RepoTestRunner extends BlockJUnit4ClassRunner {
public class RepositoryTestRunner extends Suite { public static class RepoTestRunner extends BlockJUnit4ClassRunner { private PersonRepository personRepo; RepoTestRunner(PersonRepository personRepo, Class testClass) { super(testClass); this.personRepo = personRepo; } protected Object createTest() throws Exception { return getTestClass().getOnlyConstructor().newInstance(personRepo) ; } protected void validateConstructor(List errors) { validateOnlyOneConstructor(errors); }
The result
Keeping it honest
@RunWith(RepositoryTestRunner.class) public class PersonRepositoryTest { private PersonRepository personRepository; public PersonRepositoryTest(PersonRepository personRepository) { this.personRepository = personRepository; public void should_filter_people() { Person person = Person.withName("Darth Vader"); Person nonMatching = Person.withName("Anakin Skywalker"); personRepository.savePerson(person); personRepository.savePerson(nonMatching); assertThat(personRepository.findPeople("vade")).contains(person).excludes(nonMatching); }
Part III
The big picture
Worth it?
Thought experiment: What if you could test your whole application without connecting to anything?
Maintain fakes or Speed up realz?
Maintain fakes or Speed up realz?
Other uses
public class XmlSerializationTest { private final File xmlFile; public XmlSerializationTest(File xmlFile) { this.xmlFile = xmlFile; public void serializedShouldMatch() { assertThat(normalize(Xml.read(xmlFile).toXML())).isEqualTo(normalize(slurp(xmlFile))); }
Conclusions
There’s more to tests than “integration test” or “unit test”
Get fast feedback Detect mistakes Only fail when you should
Thank you
“Integration tests or unit tests”
Integration tests or unit test?
public class JdbcPersonRepositoryTest { private static DataSource public static void createDataSource() { dataSource = JdbcConnectionPool.create(…); JdbcPersonRepository.createDatabaseSchema(dataSource); } PersonRepository personRepo = new public void should_save_people() { Person person = Person.withName("Johannes Brodwall"); personRepo.savePerson(person); assertThat(personRepo.findPeople(null)).contains(person); }
“Integration tests” Connects to services, database, files? Or just integrates whole system? Or integrates system with all services?
“Integration tests” are Slow Dependent on external systems Realistic?
Integration tests or unit test?
public class PersonTest public void equality() { assertThat(Person.withName("Darth Vader")).isEqualTo(Person.withName("Darth Vader")).isNotEqualTo(Person.withName("Anakin Skywalker")).isNotEqualTo(new Object()).isNotEqualTo(null); } Since you’re asking: FEST-assert
“Unit tests” No resources outside JVM? Only one class? Only one method?
“Unit tests” are Very fast Coupled to implementation?
Integration tests or unit test?
Why does it matter?
Who is this talk for? You have written more than a few tests in JUnit You want to find out when you write unit test and when to write integration tests and how to use mocks well You will be able to write your own test-runner for fun and profit
What will I cover Testing considerations Unit test or integration test What are good tests Mocks versus fakes Creating your own test runner The mocked test The faked test The multi purpose test What’s next? Eventually abandoned But still in many cases – eg. Files