In this post, we will dive into the world of Spring Data repositories. We will explore how to effectively test the repositories that provide the crucial link between our application and the database.
When testing your repositories, it's essential to remember that basic CRUD (Create, Read, Update, Delete) operations are already thoroughly tested by Spring Data JPA itself. Unless you have a highly unusual use-case, you generally don't need to re-test these operations. Instead, focus on testing your custom queries and operations - those that are specific to your application.
When creating repository tests, we will utilize the following Maven dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
We will base our examples around a User
entity, which is defined as follows:
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
@Column(nullable = false)
private String username;
@NotBlank
@Column(nullable = false, unique = true)
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
}
For this User
entity, we have a UserRepository
which extends CrudRepository
and includes a custom query method:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
@Query(
value = "select id, username, email from user where email = :email",
nativeQuery = true
)
Optional<User> selectUserByEmail(@Param("email") String email);
}
Testing UserRepository
To isolate the testing of the repository layer, we use @DataJpaTest
. This annotation configures an in-memory database, Hibernate, Spring Data, and the DataSource. It also turns on SQL logging.
A common property to include when using @DataJpaTest
is javax.persistence.validation.mode=none
. This turns off the Bean Validation mode and is useful when we want our tests to focus solely on the repository's behaviour, rather than validating properties on our entities:
@DataJpaTest(
properties = {
"spring.jpa.properties.javax.persistence.validation.mode=none"
}
)
class UserRepositoryTest {
@Autowired
private UserRepository underTest;
// ...
}
Now, let's write some tests
In the first test case, we aim to ensure that the repository's save operation functions properly. We begin by creating a new User
object and saving it using our repository. Next, we retrieve the user with the findById
method and assert that it corresponds to the user we initially saved.
@Test
void itShouldSaveUser() {
User user = new User(
"Username",
"test@email.com"
);
User savedUser = underTest.save(user);
Optional<User> optUser = underTest.findById(savedUser.getId());
assertThat(optUser)
.isPresent()
.hasValueSatisfying(u -> {
assertThat(u).isEqualToComparingFieldByField(savedUser);
});
}
In the next test case, we verify that the system will not save a User
object if the email
field is null. In our application, we have enforced a requirement that a User
object must have a non-null email
field before it can be saved into the database. If the email
field is null, a DataIntegrityViolationException
is expected to be thrown. This is demonstrated in the code snippet below:
@Test
void itShouldNotSaveUserWhenEmailIsNull() {
User user = new User(
"Username",
null
);
assertThatThrownBy(() -> underTest.save(user))
.isInstanceOf(DataIntegrityViolationException.class)
.hasMessageContaining("not-null property references a null or transient value");
}
Similar to the previous test case, we are testing the repository's handling of a null username
. The User
entity class specifies that the username
field must not be null, so when we attempt to save a User
with a null username
, a DataIntegrityViolationException
should be thrown.
@Test
void itShouldNotSaveUserWhenUsernameIsNull() {
User user = new User(
null,
"test@email.com"
);
assertThatThrownBy(() -> underTest.save(user))
.isInstanceOf(DataIntegrityViolationException.class)
.hasMessageContaining("not-null property references a null or transient value");
}
In the following test case, we verify that the repository can correctly retrieve a User
by their email
. We first save a User
, and then retrieve it using our custom method, selectUserByEmail
. We assert that the retrieved User
matches the one we originally saved.
@Test
void itShouldSelectUserByEmail() {
User user = new User(
"Username",
"test@email.com"
);
User savedUser = underTest.save(user);
Optional<User> optUser = underTest.selectUserByEmail("test@email.com");
assertThat(optUser)
.isPresent()
.hasValueSatisfying(u -> {
assertThat(u).isEqualToComparingFieldByField(savedUser);
});
}
Finally, we are testing the repository's behavior when we attempt to retrieve a User
by an email
that does not exist in our database. As there is no User
with the provided email
, our selectUserByEmail
method should return an empty Optional
, which we confirm with an assertion.
@Test
void itShouldNotSelectUserByEmailWhenEmailDoesNotExist() {
// Given
String email = "test@email.com";
// Then
Optional<User> optUser = underTest.selectUserByEmail(email);
assertThat(optUser).isNotPresent();
}
Conclusion
The tests we've illustrated in the given example are classified as integration tests, which are designed to evaluate how various components within a system function in conjunction with one another. In our specific case, we are examining the interaction between the repository and the database to ensure seamless communication and data management.
It is important to note that the configuration of integration tests differs slightly from that of conventional unit tests. For instance, we employ the @DataJpaTest
annotation in place of the more commonly used @Test
annotation. This particular annotation is tailored for JPA-based tests, enabling the necessary setup for testing the persistence layer.
Moreover, instead of relying on a mock or stub database, we initialize a real database for the purpose of these tests. Although it is an H2 database operating in-memory, it provides a more accurate representation of the actual database environment. This approach allows us to thoroughly test the repository's functionality and its interaction with the database, ensuring that the system components work together as intended.
This article marks the beginning of an in-depth series focused on testing Spring Boot applications. Throughout the forthcoming chapters, we will explore various aspects of testing, including examining the Service
and Controller
layers in greater detail. Additionally, we will discuss the utilization of Testcontainers
for addressing more complex and advanced use cases.
Thank you for reading and see you in the next one!