Sometimes you want to mock the behaviour of a class to give different responses to each invocation of a method.
For example, if you wanted to arrange a test to verify the ability of a class; Syncer
to recover from a RuntimeException
thrown by a service it uses, your test might look a bit like this:
@Test
public void getData_shouldReturnSyncedData_whenSyncingAfterPreviousSyncFails() {
// given
UnreliableService mockService = mock(UnreliableService.class);
when(mockService.getApiData()).thenThrow(new RuntimeException("boom")); // arrange a failed sync
Syncer syncer = new Syncer(mockService);
syncer.sync(); // trigger the failed sync
when(mockService.getApiData()).thenReturn("a value"); // arrange a successful sync
// when
syncer.sync();
// then
assertThat(syncer.getSyncData()).isEqualTo("a value");
}
So you run this test and it fails. Not always a bad thing when developing tests; at least you know it’s doing something! But in this case, the test case is failing on this line:
when(mockService.getApiData()).thenReturn("a value");
This isn’t an assertion, so we’ve likely got something wrong in our test arrangement.
The error indicates that we’re actually throwing a RuntimeException
from our test.
java.lang.RuntimeException: boom
at com.petertackage.example.ExampleTest.getData_shouldReturnSyncedData_whenSyncingAfterPreviousSyncFails(ExampleTest.java:18)
Process finished with exit code 255
So what’s going on and what can we do about it?
Well, the problem lies in the fact that in our attempt to mock the getApiData
method for a second time, we are, perhaps surprisingly, invoking that method and getting the RuntimeException
we told it to throw in our previous mocking. It’s right there!
To fix this we need to define both mock invocations without triggering the mocked method behaviour.
Luckily we can do this safely by chaining the mocking declarations like this:
when(mockService.getApiData()).thenThrow(new RuntimeException("boom"))
.thenReturn("a value");
This prevents the RuntimeException
from being thrown prematurely and makes our test case neater too:
@Test
public void getData_shouldReturnSyncedData_whenSyncingAfterPreviousSyncFails() {
// given
UnreliableService mockService = mock(UnreliableService.class);
when(mockService.getApiData()).thenThrow(new RuntimeException("boom"))
.thenReturn("a value");
Syncer syncer = new Syncer(mockService);
syncer.sync();
// when
syncer.sync();
// then
assertThat(syncer.getSyncData()).isEqualTo("a value");
}
We can chain as many mock behaviours as we like, although if you have more than a few it might be worth thinking about the complexity of the class you are trying to test.
Thanks for reading!
This post is Creative Commons Attribution 4.0 International (CC BY 4.0) licensed.