1. OVERVIEW

Let’s say you had to write a Business Service implementation using Spring’s TransactionTemplate instead of the @Transactional annotation.

As a quick example, you need to retrieve a film reviews from external partners using RESTful APIs.
You also need to update this film-related relational database tables rows.
And lastly, you need to publish a JMS message for interested parties to process these changes.

A made up example, but the point is that you need to write a Business Logic that involves sending RESTful API requests to external services, executing multiple SQL statements, and publishing a message to a JMS Queue.

You have already realized that the RESTful API requests and publishing a JMS message shouldn’t be part of the JDBC transaction, so you end up using TransactionTemplate, and a code snippet similar to:

DefaultDvdRentalService.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class DefaultDvdRentalService implements DvdRentalService {

  private final FilmRepository filmRepository;
  private final InventoryRepository inventoryRepository;
  private final TransactionTemplate txTemplateReadWrite;
  //  Other dependencies ...

  @Override
  public Film updateFilm(Film film) {
    // External requests to retrieve 3rd party film reviews using RestTemplate
    // ...

    // Updates a film using multiple repositories in the same SQL transaction
    Film result = this.txTemplateReadWrite.execute(status -> {
      Optional<Film> optFilm = this.filmRepository.findById(film.getFilmId());
      List<Inventory> inventories = this.inventoryRepository.findByFilmFilmId(film.getFilmId());
      // ...
      // Other Spring Data JPA repositories usage.

      return optFilm.orElseThrow(() -> new RuntimeException("..."));
    });

    // Publishes a JMS message to a Queue using JmsTemplate
    // ...

    return result;
  }
// ...
}

Now you’ll need to write some unit tests.

How would you mock TransactionTemplate?
If you mock the execute() method returning a Film instance, you won’t unit test what’s happening inside the TransactionTemplate’s execute() method.

This blog post covers how to use Mockito.doAnswer() to unit test the code inside TransactionCallback’s doInTransaction(), or TransactionCallbackWithoutResult’s doInTransactionWithoutResult() implementations.

2. JUNIT 5, MOCKITO, SPRING-BOOT-STARTER-TEST VERSIONS

This blog post assumes you are JUnit 5 and Mockito 3 or higher through spring-boot-starter-test dependency included in Spring Boot 2.2.x or higher.

3. UNIT TEST CLASS

Let’s write a DefaultDvdRentalService.java happy path unit test that covers updating a film:

DefaultDvdRentalServiceTest.java:

@ExtendWith(MockitoExtension.class)
public class DefaultDvdRentalServiceTest {

  @Mock
  private FilmRepository mockFilmRepository;

  @Mock
  private InventoryRepository mockInventoryRepository;

  @Mock
  private TransactionTemplate mockTxTemplateReadWrite;

  //  Other mocked dependencies ...

  private DvdRentalService service;

  @BeforeEach
  public void setup() {
    this.service = new DefaultDvdRentalService(this.mockFilmRepository, // ...);
  }

  @Test
  public void shouldUpdateFilm() {
    // Given
    // Create test fixtures
    Film film = FilmFixtures.createFilm();
    Inventory inventory = FilmFixtures.createInventory(film);
    // ...

    // Mock dependencies behavior
    Mockito.doAnswer(new Answer<Film>() {

      @Override
      public Film answer(InvocationOnMock invocation) throws Throwable {
        TransactionCallback<Film> callback = (TransactionCallback<Film>) invocation.getArguments()[0];
        return callback.doInTransaction(Mockito.mock(TransactionStatus.class));
      }

    }).when(this.mockTxTemplateReadWrite).execute(Mockito.any());

    Mockito.when(this.mockFilmRepository.findById(10)).thenReturn(Optional.of(film));
    Mockito.when(this.mockInventoryRepository.findByFilmFilmId(10)).thenReturn(Lists.list(inventory));

    // Other mock behavior

    // When
    Film actual = this.service.updateFilm(film);

    // Then
    Assertions.assertThat(actual.getTitle()).isEqualTo("Title");
    // Other assertions

    // Mock verifications
    Mockito.verify(this.mockFilmRepository, Mockito.times(1)).findById(10);
    Mockito.verify(this.mockInventoryRepository, Mockito.times(1)).findByFilmFilmId(10);
    // Other mocks verification

    Mockito.verifyNoMoreInteractions(
      this.mockFilmRepository,
      this.mockInventoryRepository
      // Other mocked dependencies ...
    );
  }
// ...
}

The unit test still mocks the TransactionTemplate dependency.

The key here is the Mockito.doAnswer() behavior configuration.
This behavior is configured to execute the real callback.doInTransaction() lines 16 through 21 when the TransactionTemplate’s execute() method is called.

Let’s dig in a bit:

  • First:
TransactionCallback<Film> callback = (TransactionCallback<Film>) invocation.getArguments()[0];

returns the first argument passed to the execute() method. In this case, the only argument passed to execute() was the TransactionCallback Lambda implementation (lines 16 through 21).

  • And second:
return callback.doInTransaction(Mockito.mock(TransactionStatus.class));

executes the real TransactionCallback Lambda implementation (lines 16 through 21).

You need to continue configuring the unit test mocks behaviors because the TransactionCallback implementation uses FilmRepository and InventoryRepository.

Mockito.when(this.mockFilmRepository.findById(10))...
Mockito.when(this.mockInventoryRepository.findByFilmFilmId(10))...

Later, the unit test verifies the mocked dependencies methods execution, and that there shouldn’t be other mock calls, using Mockito.verify(), and Mockito.verifyNoMoreInteractions().

TransactionTemplate, TransactionCallback unit test with JUnit and Mockito TransactionTemplate, TransactionCallback unit test with JUnit and Mockito

4. CONCLUSION

Mockito.doAnswer() helps you to unit test code that uses Template with Callback design pattern.

You can mock TransactionTemplate’s execute() method and still verify behavior inside TransactionCallback’s doInTransaction(), or TransactionCallbackWithoutResult’s doInTransactionWithoutResult() implementations using Mockito.doAnswer(), for more accurate unit testing.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.