Recently I was part of team which started a green field project. Our application was a Spring Boot application. We were strictly adhering to TestDrivenDevelopemnt. Spring has good testing support. We annotated our test classes with SpringBootTest
and it worked for most of our RestControllers
and Repository
layer.
At one place we needed to perform a batch task. The task gets generated from a user request. If the computation is performed on the request thread it would lead to request timeout. So we made a decision to use Async
support provided by Spring. But then we had the following challenge :
How do we determine that the operation is done in a separate background service ?
In order to simulate the behaviour i have built the following service :
@Component
public class BatchJobService {
JavaMailSender javaMailSender;
TaskRepository taskrepo;
@Async
public Future<Void> performTask(TaskDetails request) {
TaskTemplate template = taskRepo.findByTaskName(req.taskName());
Task task = new TaskBuilder(template)
.withDetails(request)
.build();
TaskResult result = execute(task);
SimpleMailMessage message = generateMail(result);
javaMailSender.send(message);
return new AsyncResult<>(null);
}
}
In the above code we are creating a task from a template name. The task is executed and then the result is mailed to the user. The whole operation is marked as Async
. Testing this code is challenge. If we invoke the code in a unit test we would validate the interactions with the taskrepo
and the javaMailSender
. But we can’t validate if the operation is performed in background.
The next approach was to run the solution as a Spring
test. But then since Spring
would invoke the method as an Async
task. But then the challenge was how to assert method invocation. Spring
detached method invocation from our test execution. Thus test turned out to be flaky. In order to make this a repeatable test we modified it with countdown latch. :
@SpringBootTest
class BatchJobServiceTest {
@Test
void mock_mvc_should_be_set() throws Exception {
JavaMailSender mock = Mockito.mock(JavaMailSender.class);
TaskRepository mockrepo = Mockito.mock(TaskRepository.class);
service.javaMailSender = mock;
service.taskrepo = mockrepo;
CountDownLatch latch = new CountDownLatch(1);
Mockito.doAnswer((x) -> {
latch.await();
return null;
}).when(mock).send(Mockito.any(SimpleMailMessage.class));
Future<Void> voidFuture = service.perform(new TaskDetails());
assertThat(voidFuture.isDone()).isFalse();
latch.countDown();
assertThat(voidFuture.isDone()).isTrue();
Mockito.verify(mock).send(Mockito.any(SimpleMailMessage.class))
}
}
In the above test we intercepted the Mockito stub with our implementation. The implementation would wait
on a CountDownLatch
. In return we get back a Future
task which we validate for completion. As a result we can assert with confidence that our Async task is completed within the test. Additionally we added a Timeout
to the test which would fail the test in finite time instead of an indefinite wait.