Testing Repositories with Spring Boot

Testing Repositories with Spring Boot

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!

Did you find this article valuable?

Support Jarosław Samulewski by becoming a sponsor. Any amount is appreciated!