
Unit testing in Spring Webflux using StepVerifier and Mockito
In the last post I showed you how to build simple reactive web application with Spring Webflux and MongoDB from scratch. You can find it here – LINK.
But one important element is missing in this app – the tests. If you want to build high quality app, you can’t avoid them. Today I will show you how to write unit tests in Webflux and some nice features of Mockito. If you are experienced developer you might used it already – leave your feedback in comment and let me know what you think about this post !
Table of contents
- What is unit testing ?
- Webflux tools for unit testing
- Setup for tests with @Testconfiguration
- Arrange of the test data
- StepVerifier
- Mockito ArgumentCaptor
- Mockito Verify
- Summary
What is unit testing?
Unit test is the most common type of test. In test pyramid you can find it on the lowest level. Test pyramid concept describes the most optimal structure of the tests in applications – authored by Martin Fowler (link here). Unit tests are fast and cheap. They are testing the smallest piece of code in isolation and checks if the result meet requirements (expressed with assertions).
Running test in isolation means that any external resources are not allowed. If our business logic need data from that source (at example database) we can use mocking library, which creates fake objects (stubs/mocks) for testing purposes.
Tools for unit testing in Spring Webflux
Project Reactor (on which Spring Webflux is based) provides a great API for testing and offers us two useful tools for fast unit tests written in declarative manner.
- StepVerifier – creates complete test which consists of steps executed in given order
- TestPublisher – responsible for asynchronous emitting of predefined elements
I won’t cover all classes of my project with tests. For our educational purposes I will test only Team entity, with both kinds of tests – unit and integration tests (it will be described on the next post). Link to the test class
TestService does not contain any classes returning multiple elements, thus we don’t need the TestPublisher
Of course for proper unit tests we need a mocking library, I use Mockito which is included in Spring Boot by default.
Setup for tests – @Testconfiguration
At first we must include SpringExtension, which contains Mockito and some other features supporting testing in Spring (also for integration tests)
@TestConfiguration is one of these features. It give us possibility to override Spring configuration with our custom beans. It will skipped during component scan.
We can create in two ways:
- by adding static inner class with annotation. Optional exists the possibility to import configuration from external source.
- with @import annotation on the class level we can import test configuration from external source.
Elements that will be mocked must be annotated with @MockBean annotation, for class that should be tested we are creating typical Spring bean (with mocked members passed in constructor).
I create static inner with configuration for my tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@TestConfiguration public static class TeamServiceTestConfiguration { @MockBean public TeamRepository teamRepository; @MockBean public UserRepository userRepository; @Bean public TeamService teamService() { return new TeamService(teamRepository, userRepository); } } |
Then we can include all of that elements with @Autowired annotation
Arranging of the test data
At first I am arranging test data with constructor in Case of records, or with builder design pattern by normal classes.
I am avoiding pre arrange of test data (at example with method annotated with @BeforeEach annotation) for all tests. The tests should be independent and running in isolation – common data for all tests would break this rule.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
String id = "21344r23r34"; String teamId = "2r872394r578"; String teamName = "Some team"; String username = "testsusrname"; Role role = Role.DEVELOPER; var member = User.builder() .username(username) .password("some password") .teamId(teamId) .build(); var memberMono = Mono.just(member); var teamEntityMono = Mono.just(Team.builder() .id(teamId) .name(teamName) .members(List.of(member)) .build()); |
Our service methods look into db with a repository method. In unit tests we don’t have connection with any external resources, result must be mocked with Mockito.
1 2 3 4 |
Mockito.when(teamRepository.findById(Mockito.anyString())) .thenReturn(teamEntityMono); Mockito.when(userRepository.findById(id)) .thenReturn(memberMono); |
StepVerifier
StepVerifier takes our publisher (Mono or Flux) and helps us to check the result when it will be subscribed.
StepVerifier must be always initiated with create() method – publisher is given as parameter (mainly it will be the call of the method returning data wrapped in Mono/Flux type).
Then we can declare our expectations in form of chained methods, which must have terminal expectation like expectComplete(), or expectError() at the end. At the finish the StepVerifier must be verified, we can do this with the call of the verify() method. The two last steps can be done in one call of the verifyComplete() method.
Chain of expectations offers us checking the assertions in declarative manner.
We can use expectNext() method to compare two elements of the same type. By reactive streams with multiple elements each call checks next emitted object.
1 2 3 4 5 |
StepVerifier .create(teamService.findAllTeams()) .expectNext(expectedTeam1) .expectNext(expectedTeam2) .verifyComplete(); |
We can also check multiple assertions in one lambda body with .assertAll() method:
1 2 3 4 5 6 7 8 9 |
StepVerifier .create(teamService.findById(id)) .assertNext(team -> { Assertions.assertThat(team.name().equals(teamName)); Assertions.assertThat(team.members()).hasSize(1); Assertions.assertThat( team.members().get(0).username()).isEqualTo(username); }) .verifyComplete(); |
StepVerifier gives us many more methods for defining expected results. It’s really worth to dive into documentation! In the next tests I will show you some other possibilities.
If you will check one custom assertion use expectNextMatches() which takes predicate as parameter. By reactive stream with multiple elements (flux) next element can be verified with the new call of this method (like by expectNext() method)
1 2 3 4 |
StepVerifier .create(teamService.findByName(teamName)) .expectNextMatches(resultingTeam -> resultingTeam.name().equals(teamName)) .verifyComplete(); |
For testing exceptions we have also some good methods. Whith expectError() method can be tested if any exception is throwed, we can pass the type of expected exception as argument. To check message of the error expectErrorMessage(String errorMessage) can be used.
If you want to check few elements of exception at one time we can use .expectErrorMatches() which take predicate as argument. I used it in my unit tests:
1 2 3 4 5 6 7 8 9 10 11 |
@Test public void shouldReturnErrorOnGetTeamByNameWhenMissing() { Mockito.when(teamRepository.findByName(Mockito.anyString())) .thenReturn(Mono.empty()); StepVerifier .create(teamService.findByName("some name")) .expectErrorMatches(error -> error instanceof TeamServiceException && error.getMessage().equals("Team with given name doesn't exist")) .verify(); } |
Mockito.ArgumentCaptor
Sometimes we need test arguments which are not accessible from outside. It can be local variable at example. With Mockito comes practical helper for that case – ArgumentCaptor. It can capture argument passed into method, which we can then check with assertions.
At first captor for specific class must be created with ArgumentCaptor.forClass method:
1 |
ArgumentCaptor<Team> saveTeamCaptor = ArgumentCaptor.forClass(Team.class); |
We can also initiate captor on the class level with @Captor annotation, it must be done for the captors of collections. First approach will not work, because of generics limitations in Java. I used it for the users list:
1 2 |
@Captor public ArgumentCaptor<List<User>> usersCaptor; |
ArgumentCaptor is really helpful by testing of create/update methods, where entity is created from incoming Dto and then passed into save() method in repository. For capture of arguments you must only call capture() method of proper captor
1 2 |
Mockito.when(teamRepository.save(saveTeamCaptor.capture())) .thenReturn(createdTeamMono); |
For assertions, we can call getValue() method which returns captured object
1 2 3 |
// captor assertions Assertions.assertThat(TeamUtils.toMembers.apply(saveTeamCaptor.getValue())).hasSize(2); Assertions.assertThat(TeamUtils.toId.apply(saveTeamCaptor.getValue())).isEqualTo(teamId); |
Mockito .verify() and inOrder()
With verify() you can check invocations of the mock in many ways. In first argument we pass mocked class, then expected result. Mockito offers some useful method for describing the expected result, in next example we expect that method won’t be called.
Then we must call method that will be checked (with custom argument or one of Mockito’s matchers – I used any() in this case).
1 2 3 4 |
Mockito.verify(teamRepository, Mockito.never()) .save(Mockito.any()); Mockito.verify(userRepository, Mockito.never()) .saveAll(Mockito.any()); |
You can also check how many times that method was called.
1 2 |
Mockito.verify(teamRepository,Mockito.times(1)) .delete(teamId); |
We can also check the order of method’s invocations (it’s useful when business logic depends on it)
1 2 3 4 5 |
InOrder inOrder = Mockito.inOrder(teamRepository,userRepository); inOrder.verify(teamRepository, Mockito.times(1)).findByName(teamName); inOrder.verify(teamRepository,Mockito.times(1)).save(Mockito.any(Team.class)); inOrder.verify(userRepository,Mockito.times(1)).saveAll(Mockito.any()); inOrder.verify(teamRepository,Mockito.times(1)).save(Mockito.any(Team.class)); |
At the end we can check, if mocked classes have any further interactions
1 |
Mockito.verifyNoMoreInteractions(teamRepository, userRepository); |
Mockito offers lot of methods for verifying, it’s worth to look into documentation if you need check some special test cases.
Summary
TeamService class have good coverage in unit tests. It’s first step on the way into great quality of the application. But not last!
In the next post I will show you how to write reactive integration tests. It’s also important, because integration tests gives us feedback about whole data flow – from web layer, through service, database to the response. It will be exciting!
Stay aware and subscribe newsletter – you won’t miss any new programming knowledge from me 🙂