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.
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
🐳 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/