Background
Spring Boot Testing components provides great convenience for us to test our implementation. In my experience, I found projects are still relying on mocks. The reason behind it is integration tests usually takes too long and expensive.
Such opinion was true in the past. However, today with Spring’s Test component and Testcontainers, integration tests no longer being a burden. We will look into the options in implementing tests using Spring Boot for a standard REST application.
The Application
The application is rather a simple REST application which consists of Spring Data JPA, Spring Security, and Spring Web. The typical components used in most Spring Boot applications.
We will start by implementing the repository component.
Entity & Repository
Given that we have the entity User
, we will implement a Repository
class that
extends JpaRepository
.
interface UserRepository extends JpaRepository<User, Long> {}
We want to allow the users to retrieve a User
by username
. However, we want to hide their id
information and to simplify
name
- instead of having first
and last
name, we will just return their full name. For this we will use a
Projections called UserWithoutId
.
interface UserRepository extends JpaRepository<User, Long> {
@NativeQuery(
name = "User.findByUsername",
value = "SELECT CONCAT_WS(' ', first, last) as name, username, status FROM users WHERE username = ?1",
sqlResultSetMapping = "User.WithoutId"
)
Optional<UserWithoutId> findByUsername(String username);
}
Since we have a custom implementation in UserRepository
, we will implement a test to ensure that it is behaving as expected. For this we will
use @DataJpaTest
which will load sufficient components for us to run a JpaRepository
test.
We will implement two scenarios in UserRepositoryTests
- find by username with an existing username and find by username with a non-existing username.
@Testcontainers
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop")
class UserRepositoryTests {
@Container
@ServiceConnection
private static final PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
@Autowired
private UserRepository repository;
@Test
@DisplayName("Given there the username rashidi.zin exists When I find by the username Then I should receive a summary of the user")
@Sql(statements = "INSERT INTO users (id, first, last, username, status) VALUES (1, 'Rashidi', 'Zin', 'rashidi.zin', 0)")
void findByUsername() {
var user = repository.findByUsername("rashidi.zin");
assertThat(user).get()
.extracting("name", "username", "status")
.containsExactly("Rashidi Zin", "rashidi.zin", ACTIVE);
}
@Test
@DisplayName("Given there the username zaid.zin does not exist When I find by the username Then I should receive empty optional")
void findByNonExistingUsername() {
var user = repository.findByUsername("zaid.zin");
assertThat(user).isEmpty();
}
}
-
@Testcontainers
- Enabling Testcontainers support for this test -
DataJpaTest
- Load Spring Data JPA’s related components -
@Container
- Allow Testcontainers manage the lifecycle ofPostgreSQLContainer
-
@ServiceConnection
- Automatically assignDataSource
related properties -
@Sql
- Load test data
With that, we have verified that UserRepository.findByUsername
is behaving as expected. Full implementation can be found in
user
package. For other database types, I wrote articles on using @DataJdbcTest
and using @DataMongoTest
Next, we will implement the web components.
Web Implementation
-
WebSecurityConfiguration
- contains a simple HTTP Basic authentication -
UserResource
- implements aGET
method to a retrieve user information and aExceptionHandler
that will returnNOT_FOUND
for non-existingusername
:
@RestController
class UserResource {
private final UserRepository repository;
UserResource(UserRepository repository) {
this.repository = repository;
}
@GetMapping(value = "/users/{username}", produces = APPLICATION_JSON_VALUE)
public UserWithoutId findByUsername(@PathVariable String username) {
return repository.findByUsername(username).orElseThrow(InvalidUserException::new);
}
@ExceptionHandler(InvalidUserException.class)
@ResponseStatus(NOT_FOUND)
public void handleInvalidUserException() {}
static class InvalidUserException extends RuntimeException {}
}
Testing with @WebMvcTest
If long-running time is a concern, @WebMvcTest
would be a suitable approach as it will only load web related components. It allows us to mock
any of its dependencies and arrange suitable behaviour for them. In the following implementation, we will mock (or arrange) the behaviour of UserRepository.findByUsername
:
In findByUsername
, we will arrange that it will return Optional
containing UserWithoutId
. We will expect that the response will be HTTP OK
. While in findByNonExistingUsername
, we
arrange that it will return an empty Optional
. This will lead to InvalidUserException
being thrown and translated to HTTP NOT_FOUND
.
@WebMvcTest(controllers = UserResource.class, includeFilters = @Filter(EnableWebSecurity.class))
class UserResourceTests {
private static MockMvcTester mvc;
@MockitoBean
private UserRepository repository;
@BeforeAll
static void setup(@Autowired WebApplicationContext context) {
mvc = from(context, builder -> builder.apply(springSecurity()).build());
}
@Test
@WithMockUser
@DisplayName("Given username rashidi.zin exists When when I request for the username Then the response status should be OK")
void findByUsername() {
var fakeUser = Optional.of(new UserWithoutId("Rashidi Zin", "rashidi.zin", ACTIVE));
doReturn(fakeUser).when(repository).findByUsername("rashidi.zin");
mvc
.get().uri("/users/{username}", "rashidi.zin")
.assertThat()
.hasStatus(OK);
verify(repository).findByUsername("rashidi.zin");
}
@Test
@WithMockUser
@DisplayName("Given username rashidi.zin does not exist When when I request for the username Then the response status should be NOT_FOUND")
void findByNonExistingUsername() {
doReturn(empty()).when(repository).findByUsername("rashidi.zin");
mvc
.get().uri("/users/{username}", "rashidi.zin")
.assertThat()
.hasStatus(NOT_FOUND);
verify(repository).findByUsername("rashidi.zin");
}
@Test
@DisplayName("Given there is no authentication When I request for the username Then the response status should be UNAUTHORIZED")
void findByUsernameWithoutAuthentication() {
mvc
.get().uri("/users/{username}", "rashidi.zin")
.assertThat().hasStatus(UNAUTHORIZED);
verify(repository, never()).findByUsername("rashidi.zin");
}
}
-
@WebMvcTest
- Our test will only focus onUserResource
and we will load security configuration fromWebSecurityConfiguration
-
SecurityMockMvcConfigurers.springSecurity()
- Enable Spring Security support forMockMvcTester
-
@WithMockUser
- Mocks user authentication. Without it the response will beUNAUTHORIZED
as demonstrated infindByUsernameWithoutAuthentication
-
@MockitoBean
- MocksUserRepository
since we have verified that it works correctly inUserRepositoryTests
-
Mockito.verify
- Verifies thatUserRepository.findByUsername
was either triggered (when user is authenticated) or not
Given that UserResourceTests
is specifically for UserResource
and only necessary components are loaded, its execution
should be fast.
Testing with @SpringBootTest
@SpringBootTest
, by default, will load all components. In our case, it will expect there is a running PostgreSQL and the properties are assigned.
This is handled by TestcontainersConfiguration
and
we will import it into our test - FindByUsernameTests
.
We will implement the same test scenarios as we did in UserResourceTests
:
@Import(TestcontainersConfiguration.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = {
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.security.user.name=rashidi.zin",
"spring.security.user.password=jU$7d3m0pL3a$eRe|ax"
})
@Sql(executionPhase = BEFORE_TEST_CLASS, statements = "INSERT INTO users (id, first, last, username, status) VALUES (1, 'Rashidi', 'Zin', 'rashidi.zin', 0)")
class FindByUsernameTests {
@Autowired
private TestRestTemplate restClient;
@Test
@DisplayName("Given username rashidi.zin exists When I request for the username Then response status should be OK and it should contain the summary of the user")
void withExistingUsername() {
var response = restClient
.withBasicAuth("rashidi.zin", "jU$7d3m0pL3a$eRe|ax")
.getForEntity("/users/{username}", UserWithoutId.class, "rashidi.zin");
assertThat(response.getStatusCode()).isEqualTo(OK);
var user = response.getBody();
assertThat(user)
.extracting("name", "username", "status")
.containsExactly("Rashidi Zin", "rashidi.zin", ACTIVE);
}
@Test
@DisplayName("Given username zaid.zin does not exist When I request for the username Then response status should be NOT_FOUND")
void withNonExistingUsername() {
var response = restClient
.withBasicAuth("rashidi.zin", "jU$7d3m0pL3a$eRe|ax")
.getForEntity("/users/{username}", UserWithoutId.class, "zaid.zin");
assertThat(response.getStatusCode()).isEqualTo(NOT_FOUND);
}
@Test
@DisplayName("Given there is no authentication When I request for the username Then response status should be UNAUTHORIZED")
void withoutAuthentication() {
var response = restClient.getForEntity("/users/{username}", UserWithoutId.class, "rashidi.zin");
assertThat(response.getStatusCode()).isEqualTo(UNAUTHORIZED);
}
}
FindByUsernameTests
, we have:-
Import
PostgreSQLContainer
fromTestcontainers
that is defined inTestcontainersConfiguration
-
Define default username and password through
spring.security.user.name
andspring.security.user.password
-
Insert test data prior to running the class
In withExistingUsername
, we implement the same verification in UserResourceTests.findByUsername()
and UserRepositoryTests.findByUsername()
. The same goes to
withNonExistingUsername
and withoutAuthentication
whereby its verification is the same as
UserResourceTests.findByNonExistingUsername()
, UserRepositoryTests.findByNonExistingUsername()
, and UserResourceTests.findByUsernameWithoutAuthentication()
If you find this is redundant, you are right. Given that FindByUsernameTests
is an end-to-end integration test class, we could rely on solely on it. As for
implementations in UserResourceTests
and UserRepositoryTests
can be removed.
Conclusion
Wherever possible, I will always favour using @SpringBootTest
as it allows me to ensure that the whole application is behaving accordingly. However, as mentioned earlier,
if the @SpringBootTest
class takes too long to run then I’d go with @WebMvcTest
. It is less desire as the test will
be affected should the production code implementation changes. For example, a refactoring.
With @SpringBootTest
, I am able to implement my tests following Behaviour Driven Development easily as
opposed to using @WebMvcTest
as I don’t have to be concerned about the feature’s implementation.
In the end, choose the ones that provide you with the efficiency to maintain and to run your tests. Either with @SpringBootTest
or the combination of @WebMvcTest
and @DataJpaTest
.