Cloud Pak for Integration

 View Only

Implementation of Testcontainers in a Java Spring Boot Application

By Aritra Das Bairagya posted Thu August 17, 2023 08:16 AM

  
Spring Boot is the most widely recognized framework for Rapid Application Development in the software industry nowadays. Utilizing Spring's provided JPA, specifically Spring Data JPA, grants us a straightforward method to formulate database queries and assess them. However, there are situations in which performing tests on a live database becomes more rational and secure, particularly when utilizing queries that rely on a specific database platform.
Let's take an example: If we wish to test our Spring Data Repositories, a database is necessary or we can mock the data and test any of the APIs which interact with the database. Yes, we could utilize the in-memory H2 database offered by Spring Boot, but there's a drawback, H2 might not be the database we use in the actual runtime. Consequently, our integration tests wouldn't confirm if our code performs as intended in the higher environment or production environment. The apparent resolution involves utilizing the actual database for our integration tests. We could create a separate database on our local server and connect our tests against it. But it might be an operational or financial overhead to maintain a separate database in each of the environments
 
To solve this problem, there's a solution which is Testcontainers.
 

Definition:
Testcontainers is an open-source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container.

Unit tests with real dependencies:
No more need for mocks or complicated environment configurations. Define your test dependencies as code, then simply run your tests and containers will be created and then deleted. With support for many languages and testing frameworks, all we need is Docker. Use a containerized instance of our database to test our data access layer code for complete compatibility, without requiring a complex setup on developer machines.
In this article, we'll discuss the implementation of a REST API endpoint using Spring MVC, Spring Data JPA, and Postgres. And test those REST APIs using Testcontainers and RestAssured.

Prerequisites:
  1. Java 17+
  2. Any IDE (Intellij IDEA, Eclipse/STS, NetBeans, VS Code)
  3. A Docker environment supported by Testcontainers (Supported Docker Environment)

We are going to create a Spring Boot project using Spring Data JPA with Postgres and implement a REST API endpoint to return all the policy details that are stored in the database. Then we will test this API using the Testcontainers Postgres module and RestAssured.

Let's create a new Spring Boot project from Spring Initializr by selecting the Spring Web, Spring Data JPA, PostgreSQL Driver, and Testcontainers starters.

For the sake of simplicity, in the application, we'll be having one Controller, one Entity, and one Repository class. The functionality will be fetching all the Policies using JPA and testing them through TestContainers. Below will be the project structure:

 

Implementation:

STEP:1 | Required dependencies
Below are the MAVEN dependencies required to proceed:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-testcontainers</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>postgresql</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
	<groupId>io.rest-assured</groupId>
	<artifactId>rest-assured</artifactId>
	<scope>test</scope>
</dependency>
Here is the full pom.xml :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.testcontainer</groupId>
	<artifactId>TestContainer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>TestContainer</name>
	<description>Demo project for Test Container</description>
	<properties>
		<java.version>17</java.version>
		<testcontainers.version>1.18.0</testcontainers.version>
	</properties>
	<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-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
	      <groupId>org.springframework.boot</groupId>
	      <artifactId>spring-boot-testcontainers</artifactId>
	      <scope>test</scope>
	    </dependency>
	    <dependency>
	      <groupId>org.testcontainers</groupId>
	      <artifactId>junit-jupiter</artifactId>
	      <scope>test</scope>
	    </dependency>
	    <dependency>
	      <groupId>org.testcontainers</groupId>
	      <artifactId>postgresql</artifactId>
	      <scope>test</scope>
	    </dependency>
	    <dependency>
		    <groupId>io.rest-assured</groupId>
		    <artifactId>rest-assured</artifactId>
		    <scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
	    <dependencies>
	        <dependency>
	            <groupId>org.testcontainers</groupId>
	            <artifactId>testcontainers-bom</artifactId>
	            <version>${testcontainers.version}</version>
	            <type>pom</type>
	            <scope>import</scope>
	        </dependency>
	    </dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
Alternatively, below are the GRADLE dependencies required:
ext {
    set('testcontainersVersion', "1.18.0")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
}

STEP:2 | Create JPA entity:
Let's create the JPA Entity class which is basically the representation of our policy database object (Policy.java).  
package com.testcontainer.demo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "policies")
public class Policy {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, unique = true)
  private String polnumber;

  @Column(nullable = false)
  private String polstatus;

  public Policy() {}

  public Policy(Long id, String polnumber, String polstatus) {
    this.id = id;
    this.polnumber = polnumber;
    this.polstatus = polstatus;
  }

  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }

  public String getPolNumber() {
    return polnumber;
  }
  public void setPolNumber(String polnumber) {
    this.polnumber = polnumber;
  }

  public String getPolStatus() {
    return polstatus;
  }
  public void setPolStatus(String polstatus) {
    this.polstatus = polstatus;
  }
}

STEP:3 | Create Spring Data JPA repository:
PolicyRepository.java
package com.testcontainer.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.testcontainer.demo.entity.Policy;

public interface PolicyRepository extends JpaRepository<Policy, Long> {}

STEP:4 | Add schema creation script:
As we are not using any in-memory database, we need to create the Postgres database tables. Again for the sake of simplicity, in this example, we will use simple schema initialization support provided by Spring Boot (also we can use some database migration tools like Flyway or Liquibase).
 
Create a schema.sql file with the following content under the src/main/resources directory.
create table if not exists policies (
    id bigserial not null,
    polnumber varchar not null,
    polstatus varchar not null,
    primary key (id),
    UNIQUE (polnumber)
);

STEP:5 | Property/Configuration changes:
We also need to enable schema initialization by adding the following property in the src/main/resources/application.properties file.
spring.sql.init.mode=always

STEP:6 | Create REST API endpoint:
Finally, create a controller to implement a REST API endpoint to fetch all policies from the database.
PolicyController.java
package com.testcontainer.demo.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.testcontainer.demo.entity.Policy;
import com.testcontainer.demo.repository.PolicyRepository;

@RestController
@RequestMapping("/test-containers/apis")
public class PolicyController {

  @Autowired
  private final PolicyRepository repo;

  public PolicyController(PolicyRepository repo) {
    this.repo = repo;
  }

  @GetMapping("/policies")
  public List<Policy> getAll() {
    return repo.findAll();
  }
}

STEP:7 | Write the test for API endpoint:
We are going to write a test for the REST API GET /test-containers/apis/policies endpoint by starting the Spring context using the @SpringBootTest annotation and invoking the APIs using RestAssured.
In order to successfully start our Spring context we need a Postgres database up and running and configure the context to talk to that database. This is where Testcontainers comes into the picture.
 
We can use the Testcontainers library to spin up a Postgres database instance as a Docker container and configure the application to talk to that database as follows:
PolicyControllerTest.java
package com.testcontainer.demo.controller;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;
import java.util.List;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

import com.testcontainer.demo.entity.Policy;
import com.testcontainer.demo.repository.PolicyRepository;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PolicyControllerTest {

  @LocalServerPort
  private Integer port;

  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
    "postgres:15-alpine"
  );

  @BeforeAll
  static void beforeAll() {
    postgres.start();
  }

  @AfterAll
  static void afterAll() {
    postgres.stop();
  }

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

  @Autowired
  PolicyRepository policyRepository;

  @BeforeEach
  void setUp() {
    RestAssured.baseURI = "http://localhost:" + port;
    policyRepository.deleteAll();
  }

  @Test
  public void givenUrl_whenJsonResponseHasGivenValuesUnderKey_thenCorrect() {
	  
	  List<Policy> customers = List.of(
	      new Policy(null, "POL-12345", "Active"),
	      new Policy(null, "POL-12346", "Active")
	    );
	    policyRepository.saveAll(customers);
			  
	  given()
      .contentType(ContentType.JSON)
      .when()
      .get("/test-containers/apis/policies")
      .then()
      .statusCode(200)
      .body(".", hasSize(2)).log().all();
  }
}
Test Explanation:
Let's understand:
  • We have annotated the test class with the @SpringBootTest annotation together with the webEnvironment config, so that the test will run by starting the entire application on a random available port.
 
  • We have created an instance of PostgreSQLContainer using the postgres:15-alpine Docker image. The Postgres container is started using JUnit 5 @BeforeAll callback method which gets executed before running any test method within a test instance. The Postgres database runs on port 5432 inside the container and maps to a randomly available port on the host. We have registered the database connection properties dynamically obtained from the Postgres container using Spring Boot’s DynamicPropertyRegistry (please refer to the below highlighted screenshot).

     
    • We have injected the random port on which the Spring Boot application started using @LocalServerPort and registered the RestAssured baseURI (please refer to the below highlighted screenshot).
     

    • We are deleting all customer rows using JUnit 5 @BeforeEach callback method which gets executed before every test method. This will ensure a predictable data setup for every test and circumvent any kind of test pollution (please refer to the below highlighted screenshot).
     

    • Finally, in the givenUrl_whenJsonResponseHasGivenValuesUnderKey_thenCorrect() test, we have initialized the test data and invoked the GET /test-containers/apis/policies API endpoint and verified that 2 policy records are returned from the API (please refer to the below highlighted screenshot).

     

    STEP:8 | Run the test:
    Now whenever we'll be running the test, it'll use temporary docker images to execute the tests as below. Make sure the docker is running, and run the test.
     

    Connected to local docker and successfully created all the required containers.
     

    The test is successfully completed.
     

    During the test, below the containers are created and once the test is done, it'll be destroyed by itself as we mentioned in the test case (@AfterAll annotation)

     

    Conclusion
    We've just witnessed the most straightforward and easiest method to set up PostgreSQL for Spring Boot testing using Testcontainers. With the help of Testcontainers, we can test our application against the infrastructure that is used at runtime, without any setup costs, it is quite easy to use for the most common use cases. The configuration would be almost the same for other databases. The Testcontainers library helped us to write integration tests by using the same type of database, Postgres, that we use in production as opposed to Mocks or in-memory databases. As we are not using mocks and talking to real services, we are free to do any code refactoring and still ensure that the application is working as expected.
    In summary, Spring Boot and Testcontainers make for an exceptionally adaptable pairing.
     

    Resources
    0 comments
    119 views

    Permalink