DB Integration Tests with Spring Boot and Testcontainers
Hello. In this tutorial, we will explore the Testcontainers to perform integration tests for the jpa repositories in a Spring Boot app.
You can also check this tutorial in the following video:
1. Introduction
Testcontainers is a library that provides a clean way for writing the integration and end-to-end tests for the jpa repositories. In this example, we will create a simple rest api application to persist the data in the database and make use of the Postgres test container to have the jpa testing. For test containers to work ensure to have the Docker up and running on your machine. If someone needs to go through the Docker installation, please watch this video.
2. DB Integration Tests with Spring Boot and Testcontainers
Let us dive into some practice stuff and I am hoping that you are aware of the spring boot basics.
2.1 Tools Used for Spring boot application and Project Structure
We are using Eclipse Kepler SR2, JDK 8, and Maven. In case you’re confused about where you should create the corresponding files or folder, let us review the project structure of the spring boot application.
Let us start building the application!
3. Creating a Spring Boot application
Below are the steps involved in developing the application.
3.1 Maven Dependency
In the pom.xml
file we will define the required dependencies.
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <artifactId>SpringbootTestcontainers</artifactId> <build> <plugins> <plugin> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <artifactId>lombok</artifactId> <groupId>org.projectlombok</groupId> </exclude> </excludes> </configuration> <groupId>org.springframework.boot</groupId> </plugin> </plugins> </build> <dependencies> <dependency> <artifactId>spring-boot-starter-data-jpa</artifactId> <groupId>org.springframework.boot</groupId> </dependency> <dependency> <artifactId>spring-boot-starter-web</artifactId> <groupId>org.springframework.boot</groupId> </dependency> <dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> </dependency> <dependency> <artifactId>postgresql</artifactId> <groupId>org.postgresql</groupId> <scope>runtime</scope> </dependency> <dependency> <artifactId>lombok</artifactId> <groupId>org.projectlombok</groupId> <optional>true</optional> </dependency> <dependency> <artifactId>spring-boot-starter-test</artifactId> <groupId>org.springframework.boot</groupId> <scope>test</scope> </dependency> <dependency> <artifactId>junit-jupiter</artifactId> <groupId>org.testcontainers</groupId> <scope>test</scope> </dependency> <dependency> <artifactId>postgresql</artifactId> <groupId>org.testcontainers</groupId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <artifactId>testcontainers-bom</artifactId> <groupId>org.testcontainers</groupId> <scope>import</scope> <type>pom</type> <version>${testcontainers.version}</version> </dependency> </dependencies> </dependencyManagement> <description>Demo project for Spring Boot and Testcontainers</description> <groupId>com.springboot</groupId> <modelVersion>4.0.0</modelVersion> <name>SpringbootTestcontainers</name> <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <relativePath/> <version>2.5.6</version> <!-- lookup parent from repository --> </parent> <properties> <java.version>1.8</java.version> <testcontainers.version>1.16.0</testcontainers.version> </properties> <version>0.0.1-SNAPSHOT</version> </project>
3.2 Application properties file
Create a properties file in the resources
folder and add the following content to it. The file will contain information about the database connectivity and spring jpa. For this tutorial, we will use the Postgresql database. I already have the container up and running on the localhost:5433
.
application.properties
server.port=9800 spring.application.name=springboot-and-testcontainers #database settings spring.datasource.username=your_db_username spring.datasource.password=your_db_password ##sample url - jdbc:postgresql://hostname:port/your_db_name spring.datasource.url=your_db_url spring.datasource.driver-class-name=org.postgresql.Driver #jpa settings spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false
3.3 Java Classes
Let us write the important java class(es) involved in this tutorial. The other non-important classes for this tutorial like the main, controller, service, exceptions, and bootstrap can be downloaded from the Downloads section.
3.3.1 Model class
Create a model class that will be responsible for schema and data in the sql table.
Book.java
package com.springboot.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; // entity table. //lombok @Data @NoArgsConstructor @AllArgsConstructor @Builder //spring @Entity @Table(name = "book") @Component public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) int id; String author; String title; String genre; String publisher; int quantity; }
3.3.2 Repository interface
Add the following code to the repository interface that extends the JpaRepository
. The interface consists of custom methods to clearly understand the jpa and test container implementation.
BookRepository.java
package com.springboot.repository; import com.springboot.model.Book; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface BookRepository extends JpaRepository<Book, Integer> { // custom jpa method to find books by genre. List<Book> findBooksByGenre(String genre); // custom jpa method to find books by quantity. List<Book> findBooksByQuantityGreaterThanEqual(int quantity); // custom jpa method to find a book by name. Book findFirstByTitle(String title); }
3.4 Test cases implementation
To set up the test container in this tutorial we will need Docker (for pulling the image used by the test container) and the required testcontainer
library. The dependency is already added to the pom.xml
file.
3.4.1 Starting and stopping the container
Spring boot provides a feature called the slice test which is a neat way to test the horizontal slices of the application. To test the jpa we will make use of the Book repository interface created above. Now to configure a database that is exclusively available for our tests we will create a BaseIT
class in the test folder.
BaseIT.java
package com.springboot.repository; import org.testcontainers.containers.PostgreSQLContainer; // using the singleton container approach to improve the performance of our tests. public abstract class BaseIT { static PostgreSQLContainer<?> container; static { container = new PostgreSQLContainer<>("postgres:alpine") .withUsername("duke") .withPassword("password") .withDatabaseName("container") .withReuse(true); container.start(); } }
3.4.2 Running the tests
With this done we can write our tests. We now will create a class named BookRepositoryTest.java
that will extend the BaseIT
class. The class will consist of the test cases which we can run as Junit tests.
BookRepositoryTest.java
package com.springboot.repository; import static org.assertj.core.api.Assertions.assertThat; import static org.testcontainers.shaded.org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import com.github.javafaker.Faker; import com.springboot.model.Book; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; // annotation is used to test the jpa repositories // by default uses the embedded in-memory database for testing @DataJpaTest // annotation used to configure a test database instead of application // defined or auto-configured datasource @AutoConfigureTestDatabase(replace = Replace.NONE) class BookRepositoryTest extends BaseIT { private static final Faker FAKER = new Faker(Locale.ENGLISH); @Autowired BookRepository objUnderTest; @Test void shouldFindBookById() { Book actual = create(randomAlphabetic(5), randomAlphabetic(5), 1); objUnderTest.saveAndFlush(actual); Book expected = objUnderTest.findById(actual.getId()).get(); assertThat(expected).usingRecursiveComparison().isEqualTo(actual); } @Test void shouldFindBooksByGenre() { String genre = "Fable"; List<Book> actual = prepare(2, randomAlphabetic(5), genre, 10); objUnderTest.saveAllAndFlush(actual); List<Book> expected = objUnderTest.findBooksByGenre(genre); assertThat(expected).usingRecursiveComparison().isEqualTo(actual); } @Test void shouldFindBooksByGenre_ReturnAnEmptyList() { List<Book> actual = prepare(2, randomAlphabetic(2), "Fiction", 1); objUnderTest.saveAllAndFlush(actual); assertThat(objUnderTest.findBooksByGenre(randomAlphabetic(5))).isEmpty(); } @Test void shouldFindBooksByQuantity() { int quantity = 60; List<Book> actual = prepare(5, randomAlphabetic(5), randomAlphabetic(5), quantity); objUnderTest.saveAllAndFlush(actual); List<Book> expected = objUnderTest.findBooksByQuantityGreaterThanEqual(quantity); assertThat(expected).usingRecursiveComparison().isEqualTo(actual); } @Test void shouldFindBooksByQuantity_ReturnAnEmptyList() { List<Book> actual = prepare(2, randomAlphabetic(2), randomAlphabetic(5), 3); objUnderTest.saveAllAndFlush(actual); assertThat(objUnderTest.findBooksByQuantityGreaterThanEqual(50)).isEmpty(); } @Test void shouldFindFirstBookByTitle() { Book book1 = create("Harry Potter", "Fantasy Fiction", 5); Book book2 = create("Harry Potter", "Fantasy Fiction", 10); List<Book> actual = Arrays.asList(book1, book2); objUnderTest.saveAllAndFlush(actual); assertThat(objUnderTest.findAll().size()).isEqualTo(2); Book expected = objUnderTest.findFirstByTitle("Harry Potter"); assertThat(expected).usingRecursiveComparison().isEqualTo(book1); } //helper methods. private List<Book> prepare(int iterations, String title, String genre, int quantity) { List<Book> books = new ArrayList<>(); for (int i = 0; i < iterations; i++) { books.add(create(title, genre, quantity)); } return books; } private Book create(String title, String genre, int quantity) { return Book.builder() .author(FAKER.book().author()) .title(title) .genre(genre) .publisher(FAKER.book().publisher()) .quantity(quantity) .build(); } }
4. Run the Testcases
To execute the repository test cases, right-click on the BookRepositoryTest.java
class, Run As -> Junit Tests
. If everything goes well the test cases will be passed successfully as shown in Fig. 2. The test container will download the given docker image to run the repository test cases.
5. Project Demo
Run the implementation file (i.e. TestContainersApp.java
). To test the application endpoints we will use the postman tool. However, you’re free to use any tool of your choice for interacting with the application endpoints.
Application endpoints
-- get a book by id -- http://localhost:9800/book/id?key=1 -- get books -- http://localhost:9800/book/all -- get books by genre -- http://localhost:9800/book/genre?type=Fable -- get books by quantity -- http://localhost:9800/book/quantity?quantity=5
That is all for this tutorial and I hope the article served you whatever you were looking for. Happy Learning and do not forget to share!
6. Summary
In this tutorial, we learned the implementation of test containers to test the repository self or custom methods. You can download the sample application as an Eclipse project in the Downloads section.
7. Download the Project
This was an example of test containers implementation in a sping application to test the repository methods.
You can download the full source code of this example here: DB Integration Tests with Spring Boot and Testcontainers