Beside unit tests, it is a good idea to have some integration tests. An integration test only makes sure that the new feature is in place. An integration test does not ponder on edge cases. It’s crucial to keep an integration test as simple as possible.
Make the Greeter App Modular
The greeter app from the Test Driven Development is not very modular and hence it is not possible to write integration tests for the new greeter app. In fact, in the first TDD blog post we only could write a system test to check if reading from the keyboard is working.
For the integration test, we need a ReadAndGreet module where we read from a scanner and return the greeting text. Let’s transform the greeter app from the second test driven development post
class Hello {
void main() {
Scanner input = new Scanner(System.in);
String name = input.next();
System.out.println("Hello " + name);
}
}
to
public class ReadAndGreet {
private Scanner scanner;
public ReadAndGreet(Scanner scanner) {
this.scanner = scanner;
}
public getGreeting() {
return "Hello " + scanner.next());
}
}
and
class Hello {
void main() {
Scanner input = new Scanner(System.in);
readAndGreet = new ReadAndGreet(input);
System.out.println(readAndGreet.getGreeting());
}
}
Write a Failing Integration Test
And now we can write a simple failing integration test for the not yet integrated Greeter feature, which says "Good morning", "Good afternoon" or "Good evening" depending on the day time.
public class ReadAndGreetIntegrationTest {
@Mock
Scanner scanner;
@Test
void shouldGreetTomWithGoodMorning() {
when(scanner.next).thenReturn("Tom");
ReadAndGreet readAndGreet = new ReadAndGreet(scanner);
assertEquals("Good morning Tom", readAndGreet.getGreeting())
}
}
Perfect works… but wait, what if the test runs in the afternoon? It will fail and we don’t want to test the edge cases. So we need to relax the test a bit, we are only interested in "Good" and "Tom" and omit the in-between "morning", "afternoon" and "evening".
public class ReadAndGreetIntegrationTest {
@Mock
Scanner scanner;
@Test
void shouldGreetTomWithGoodMorning() {
when(scanner.next).thenReturn("Tom");
ReadAndGreet readAndGreet = new ReadAndGreet(scanner);
assertThat(readAndGreet.getGreeting(), MatchesPattern("^Good .* Tom$", ))
}
}
Now we have a failing and relaxed integration test because we don’t have our Greeter in place yet. That is the next step
public class ReadAndGreet {
private Scanner scanner;
private Greeter greeter;
public ReadAndGreet(Scanner scanner) {
this.scanner = scanner;
this.greeter = new Greeter();
}
public getGreeting() {
return greeter.sayHelloTo(scanner.next());
}
}
And the integration test is happy. You have now a test in place that will act as an alarm if somebody accidentally removes the Greeter from your code. Or if somebody replaces the Greeter with the old "Hello" greeter functionality. Furthermore writing tests forces you to make your code modular which leads to more reusable code, think of a different scanner than the standard input. So again testing helps you in the microarchitecture and favors simple code over complex code.
If you have questions or improvements or any other kind of suggestions let me know in the comments below.
If you are interested in an online or onside workshop for test driven development write me an email artificials_ch@gmx.ch