Spring Boot MySQL integration tests with Testcontainers

Spring Boot MySQL integration tests with Testcontainers

When it comes to writing database integration tests with Spring Boot, there are two options: an in-memory database or Testcontaienrs. As we already covered Testing Spring Data Repositories with H2 in-memory database, we shift the gear in this article and focus on writing Spring Boot MySQL integration tests with Testcontainers.

Introduction

Although utilizing an in-memory database is simple and easy, it poses some serious shortcomings. It does not support specific features of MySQL. For instance, the STRAIGHT_JOIN. Additionally, an in-memory database does not support migration scripts. As a result, many areas of the code cannot be tested, and the tests will not be similar to the production environment.

A better approach to writing integration tests for MySQL is using Testcontainers. With Testcontainers, we can run migration scripts and test any MySQL-specific features thoroughly and hassle-free.

In the rest of the article, we cover how to set up MySQL Testcontainers and write some tests with examples.

The code under the test

We have a User entity and repository that are used by the UserService. Our objective is to write some integration tests for the service layer.

The following is the code from the User, UserRepository, and UserService classes.

package com.madadipouya.springkafkatest.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;

@Entity()
@Table(name = "users")
public class User {

    @Id
    private String id;

    @NotBlank
    @Column(name = "first_name", nullable = false)
    private String firstName;

    @NotBlank
    @Column(name = "last_name", nullable = false)
    private String lastName;

    protected User() {

    }

    public User(String id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

The repository code:

package com.madadipouya.springkafkatest.repository;

import com.madadipouya.springkafkatest.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<User, String> {

    List<User> getByFirstNameIgnoreCaseOrderByFirstNameAscLastNameAsc(String firstName);
}

The user service implementation,

package com.madadipouya.springkafkatest.service.impl;

import com.madadipouya.springkafkatest.dto.User;
import com.madadipouya.springkafkatest.repository.UserRepository;
import com.madadipouya.springkafkatest.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DefaultUserService implements UserService {

    private static final Logger logger = LoggerFactory.getLogger(DefaultUserService.class);

    private final UserRepository userRepository;

    public DefaultUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void save(User user) {
        logger.info("Saving user with id = {}", user.getUuid());
        userRepository.save(new com.madadipouya.springkafkatest.entity.User(user.getUuid(), user.getFirstName(), user.getLastName()));
    }

    @Override
    public List<com.madadipouya.springkafkatest.entity.User> getUsers(String firstName) {
        return userRepository.getByFirstNameIgnoreCaseOrderByFirstNameAscLastNameAsc(firstName);
    }
}

Add Testcontainers dependencies

To write Testcontainers tests, first, we need to add Testcontainers dependencies to the project as follows:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>

Writing Spring Boot MySQL integration tests with Testcontainers

Everything is ready now to write some awesome Testcontainers. The following code snippet is a fully functional repository test with Testcontainers.

package com.madadipouya.springkafkatest.kafka.service;

import com.madadipouya.springkafkatest.dto.User;
import com.madadipouya.springkafkatest.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import java.util.List;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Testcontainers
@SpringBootTest
public class UserServiceTest {

    @Container
    static KafkaContainer kafkaContainer = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"));

    @Container
    static MySQLContainer mySQLContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0-debian"));

    @DynamicPropertySource
    static void kafkaProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafkaContainer::getBootstrapServers);
        registry.add("spring.datasource.url", () -> mySQLContainer.getJdbcUrl());
        registry.add("spring.datasource.driverClassName", () -> mySQLContainer.getDriverClassName());
        registry.add("spring.datasource.username", () -> mySQLContainer.getUsername());
        registry.add("spring.datasource.password", () -> mySQLContainer.getPassword());
        registry.add("spring.flyway.enabled", () -> "true");
    }

    @Autowired
    private UserService userService;

    @Test
    void testSaveUser() {
        userService.save(new User(UUID.randomUUID().toString(), "John", "McClane"));
        userService.save(new User(UUID.randomUUID().toString(), "Chandler", "Bing"));
        userService.save(new User(UUID.randomUUID().toString(), "Joey", "Tribbiani"));
        userService.save(new User(UUID.randomUUID().toString(), "John", "Kennedy"));

        List<com.madadipouya.springkafkatest.entity.User> users = userService.getUsers("John");

        assertNotNull(users);
        assertEquals(4, users.size());
        assertEquals("Kennedy", users.get(0).getLastName());
        assertEquals("McClane", users.get(1).getLastName());
        assertEquals("Rambo", users.get(2).getLastName());
        assertEquals("Wick", users.get(3).getLastName());
    }

    @Test
    void testSaveUserThrowsExceptionOnDuplicateFirstNameAndLastName() {
        assertThrows(DataIntegrityViolationException.class,
                () -> userService.save(new User(UUID.randomUUID().toString(), "John", "Wick")),
                "Duplicate entry 'John-Wick' for key 'users.uc_user_first_last_name");
    }
}

Note that a test could use multiple containers, depending on the needs. In the above example, we had to run both Kafka and MySQL containers for tests to pass.

We utilized the @DynamicPropertySource to overwrite some properties on the fly. That is needed when the Testcontainers Docker port (JDBC connection) is generated randomly. It is better to keep it that way and avoid setting a predetermined port since it might conflict with another already running container.

Another crucial aspect of the above test is that it runs the Flyway migration scripts since we set the spring.flyway.enabled property to true. If you are interested to know more about Flyway, read this article.

Conclusion

In this article, we covered how to write some Spring Boot MySQL integration tests with Testcontainers. The sample project code is available on GitHub: https://github.com/kasramp/spring-kafka-test.

Inline/featured images credits