Integration Testing with Springboot – Docker and Tests Containers


Introduction

Earlier this week, I had a meeting with my colleagues about JUnit 5 and Testcontainers. I should have written a blog post about it, but for various reasons it didn’t happen, but the time has finally come, and it’s about Testcontainers. In this article, I will talk about theory but also practice, and we will see together how to set up integration tests with JUnit and Testcontainers.

What are Testcontainers

Testcontainers was started as an open source project for Java platform(testcontainers-java) but has gradually received support for more languages such as Go, Rust, dotnet (testcontainers-dotnet), node (testcontainers-node) and a few more. These are projects of varying quality, but many of them can be used without problems in your applications.
If we refer to the official documentation of Testscontainers we can also say that :

Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

What problem does it solve

If we look at different tests strategies, usually explained as a test pyramid, at the bottom we have unit tests (a unit test is a way of testing a unit, the smallest piece of code that can be logically isolated in a system). It focuses on a single component and replaces all dependencies with Mocks or Test Doubles.

Tests Pyramid from Unit to E2E

Now, if we look above the Unit Test Layer in the test-pyramid we will find the Integration test layer (which is what we are here for); which means that we will have more than one component to test, and sometimes we need to integrate with and external resource aka Backing service i.e. Databases, Message Brokers, SMTP Services and Caching Systems.
So, we can say that, a backing service is any service that our application consumes over the network, and it’s now Testcontainers come in action. Before going to the practice, let’s look at what problem we tend to solve.

⚗️ Problem

Many times when I was working on projects, we (co-workers and I) have tried to solve the problem of interacting with external resources in different ways, depending on the type of backing service.
So we arise with some several ideas, such as, should we use a virtual machine, local process or an embedded in-memory service ?

Every time we saw that each strategy has its own pros and cons and today, embedded resource is probably the most widely used method. It solves many problems, but unfortunately it comes with some new. For example, H2 In-memory database emulate the target resource but not the actual behaviour of a production database.

🔍 Why you should look at Testcontainers

One solution which works very well is Testcontainers.
With Testcontainers we can start and stop one or more docker containers with the same config and behaviour as we use in our production environment. Later in this article, I will show you, how you can configure and use it for an integration test of a Spring Boot application.

📈 Benefits

  • Software delivery and testing more predictable – This means you can use the same environment (a container) to host your software whether you are building, testing or deploying software in production. Also, containerization uses fewer resources than virtual machines, making it the go-to solution.
  • What you test is what you get – This means the containerization provides a consistent application environment for software testing and for deployment. You can be sure that the testing will accurately reflect how is the application in production (with a couple of exceptions) because the test and production environments are the same.
  • Simpler test branches – Software testers often test multiple versions of an application. They might have to test Windows and Linux versions, for example. The versions need to descend from the same codebase, but are tested and deployed separately.

📉 Drawbacks

  • Containers are not hardware-agnostic – There are few disadvantages of using containers to do software testing. For example, a containerized application can behave in different ways depending on the GPU that his server host contains and whether the application interacts with the GPU. Then, I used to think, is the role of QA Teams to test containerized apps on all the different hardware profiles that one uses in production.
  • Testing microservices – Personally I haven’t work with containerized microservices, but some developers whose have work with that, said that it’s very challenging to test microservices with test containers because you need to configure automated tests to cover multiple microservices.

Tests containers with Springboot

For this article we will use a simple Spring Boot app, which will expose 2 URLs, GET and POST for retrieving and saving data in our database. And we will send request to those URLs with integration tests made by Testcontainers.

⚙️  Setting up the application

As I said above in the prerequisite section, you will ensure that you have all the dependencies that we need to complete this tutorial.

Let’s start with the JPA Entity

@Entity
@Getter
@Setter
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "author")
    private String author;

    @Column(name = "title")
    private String title;

    @Column(name= "publication_year")
    private int year;
}

Book.java

Setting up our repository interface

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {}

BookRepository.java

Our service

@Service
@RequiredArgsConstructor
public class BookService {

    public final BookRepository repository;

    public Book saveBook(Book book) {
        return this.repository.save(book);
    }

    public List<Book> getBooks() {
        return this.repository.findAll();
    }
}

BookService.java

Our controller

@RestController
@RequiredArgsConstructor
public class BookController {

    private final BookService service;

    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping("/create/book")
    public Book createTodo(@Valid @RequestBody Book book) {
        return this.service.saveBook(book);
    }

    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/fetch/books")
    public List<Book> createTodo() {
        return this.service.getBooks();
    }
}

BookController.java


🗄  Setting up database container

Once we have design our basic “book” app, now we will configure our docker compose file, for our database service.

version: '3.8'
services:
  postgres_db:
    container_name: 'postgresdb'
    image: postgres:14.2-alpine
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=book_db
    ports:
      - '5432:5432'

docker-compose.yaml

Next, we will configure our application.yml file, to initialize our database schema in our postgres_db container.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/book_db
    username: postgres
    password: postgres

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQL82Dialect

application.yaml

Now that we have everything in place, we can start our application to verify that our configuration is OK.

Let’s start by running our database container service, in the docker-compose file directory run this command

docker-compose up -d

Once the Postgres container started, run the Spring Boot app, with this command

mvn spring-boot run

To test that our application works and saves book entity(ies) in the Postgres container DB, we can run a curl command to perform a POST Request exposed by our controller

curl -d '{"author":"John Doe", "title":"Comic Code", "year":2008}' -H "Content-Type: application/json" -X POST <http://localhost:8080/create/book>

Good, our application insert data inside the container database

Postgres database container


🐳  Setting up the Testcontainers

Once our application started, we can stop it, we don’t really need the application running to make integration tests.

So, the convenient way to write integration test with Testcontainers is to create an Abstract class, which his purpose is to set up one database container for all our tests methods, using the singleton pattern.

public abstract class AbstractIntegrationTest {

    private static final PostgreSQLContainer POSTGRES_SQL_CONTAINER;

    static {
        POSTGRES_SQL_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.2-alpine"));
        POSTGRES_SQL_CONTAINER.start();
    }

    @DynamicPropertySource
    static void overrideTestProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES_SQL_CONTAINER::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES_SQL_CONTAINER::getUsername);
        registry.add("spring.datasource.password", POSTGRES_SQL_CONTAINER::getPassword);
    }
}

The DynamicPropertyRegistry overide our application-test.properties in the ressource folder, with value in the container static methods.

# POSTGRESQL Connection Properties for Testcontainers overrided by DynamicPropertyRegistry
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=

application-test.properties

Our integration test class

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class BookPostgresSQLTest extends AbstractIntegrationTest {

    @Autowired
    private BookRepository bookRepository;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        bookRepository.deleteAll();
    }

    @Test
    @Order(1)
    void should_be_able_to_save_one_book() throws Exception {
        // Given
        final var book = new Book();
        book.setAuthor("Alain de Botton");
        book.setTitle("The school of life");
        book.setYear(2012);

        // When & Then
        mockMvc.perform(post("/create/book")
                        .content(new ObjectMapper().writeValueAsString(book))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.author").value("Alain de Botton"))
                .andExpect(jsonPath("$.title").value("The school of life"))
                .andExpect(jsonPath("$.year").value("2012"));
    }

    @Test
    @Order(2)
    void should_be_able_to_retrieve_all_book() throws Exception {
        // Given
        bookRepository.saveAll(List.of(new Book(), new Book(), new Book()));

        // When
        mockMvc.perform(get("/fetch/books")
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray());

        // Then
        assertThat(bookRepository.findAll()).hasSize(3);
    }

}

BookPostgresSQLTest.java

Foremost, we have to clean our test container before each test method, then we test that we can save a book through our controller rest method, and secondly we test that we can retrieve all books from our Testcontainers database.

🧪  Running our Integration Tests

To perform our integration test, we can use the command below

mvn -Dtest=BookPostgresSQLTest test


Then we can see that our two test passed successfully

Summary

In this post, we have seen :

  • The concept of Testcontainers
  • How to create a Spring Boot app with containerized database
  • What are advantages and Drawbacks of tests containers
  • How to write integration tests with Testcontainers

The code source of this article can be found via the link below

https://github.com/1kevinson/BLOG-TUTOS/tree/master/Java/testcontainers

Credits: https://1kevinson.com/integration-testing-with-springboot-docker-and-tests-containers/

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments