Say No to H2, Yes to Test-Container

06 / May / 2023 by anil.gola 0 comments

Developers get confused over the priority of unit test cases and integration test cases. Though both are important, when we are building services for clients, we are on a tight schedule, so completing the development is itself a challenge, let alone writing test cases.

I always strive to write test cases, irrespective of the time we have. I stress writing Integration test cases over Unit test cases in the first place. In Unit test cases, we are looking to cover a critical piece of code and mock all its surrounding logic. But in production, surrounding matters as much as that piece of code.

To write Integration tests, we cannot mock repository layers, and we need some mechanism to tackle this. The in-memory H2 database has done the job for years, though very badly. We were never able to replicate the production database for our test cases.

It’s very common, that while writing sql queries, we may be using a lot of native MySQL syntax, which H2 may not support. Windows functions or analytical functions which are quite common is not well supported by H2. Support for latest sql features comes late in H2 database. Sometime, I tell my team, try to use standard sql functions over mysql native functions and make sure that those functions are well supported on H2 so that we covered those code snippets in test cases. This is bad. This is not on.

Test Containers to the rescue.

Now we have test containers for nearly every kind of database with their latest version. So now our test database mimics the production database, and I can take full advantage of the underlying database I am using.

We will now see how easy it is to set up test containers for testing in Spring Boot.

Let’s look into our pom.xml

<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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- main dependencyies ---------->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
<!-- main dependencyies ---------->

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.0.2</version>
</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>

Main dependency has been marked in comments.

Now consider the OrderEntity.java, OrderEntityRepository.java and OrderController.java class

package org.anil.testcontainers.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name ="order_entity")
public class OrderEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderName;
    private Long orderType;
    @Transient
    private String orderTypeDes;
}
package org.anil.testcontainers.repository;

import org.anil.testcontainers.entity.OrderEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderEntityRepository  extends JpaRepository<OrderEntity,Long> {
}
@RestController
public class OrderController {
    @PersistenceContext
    EntityManager entityManager;

    @Autowired
    OrderEntityRepository orderEntityRepository;

    @PostMapping("/save")
    public ResponseEntity<OrderEntity> saveOrderEntity(@RequestBody OrderEntity orderEntityDto){
       OrderEntity orderEntity = new OrderEntity();
       orderEntity.setOrderName(orderEntityDto.getOrderName());
       orderEntity.setOrderType(orderEntityDto.getOrderType());
        orderEntityRepository.save(orderEntity);
       return ResponseEntity.ok(orderEntity);
    }

    @GetMapping("/allorders")
    public ResponseEntity<List<OrderEntity>> saveOrderEntity(){
        var query  =entityManager.createNativeQuery("""
                    select id, 
                    order_name,
                    order_type,
                    if(order_type = '1','FullFilled','Cancelled') from order_entity ; 
            """);
        // deliberately used if function which is native to mysql
        // not available in h2 db.
       var resultList = (List<Object[]>) query.getResultList();
       List<OrderEntity> list = new ArrayList<>();
       for(Object[] object : resultList){
           var id = Long.valueOf(object[0].toString());
           var orderName = object[1].toString();
           var orderType = Long.valueOf(object[2].toString());
           var orderTypeDesc = object[3].toString();
           OrderEntity orderEntity = new OrderEntity();
           orderEntity.setId(id);
           orderEntity.setOrderTypeDes(orderTypeDesc);
           orderEntity.setOrderType(orderType);
           list.add(orderEntity);
       }
       return ResponseEntity.ok(list);
    }

We would be writing integration test cases for the above Controller’s methods.

In OrderControllerTest.java we will first configure our MySQL test container.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@Testcontainers
class OrderControllerTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    @Autowired
    WebTestClient webTestClient;

    @Autowired
    private OrderEntityRepository  orderEntityRepository;

    static {
        MY_SQL_CONTAINER = new MySQLContainer("mysql:latest");
        MY_SQL_CONTAINER.start();
    }

We need first to annotate our Test class with @Testcontainers, and then in the static block, we have to create a new MySQLContainer with the latest MySQL Docker image. For the first time, it will pull the MySQL image with the tag “latest.” Docker should be installed on your system to work with containers.

And then, we start the MySQL container with a start () method. When the test cases are finished, this MySQL container will be stopped and removed from Docker’s list. Just like in-memory H2, the container will be up and tear down on the test exit.

We need to tell Spring Boot about the properties of this database for the test container, and Spring has a nice way to do this.

@DynamicPropertySource
    static void configureTestProperties(DynamicPropertyRegistry registry){
        registry.add("spring.datasource.url",() -> MY_SQL_CONTAINER.getJdbcUrl());
        registry.add("spring.datasource.username",() -> MY_SQL_CONTAINER.getUsername());
        registry.add("spring.datasource.password",() -> MY_SQL_CONTAINER.getPassword());
        registry.add("spring.jpa.hibernate.ddl-auto",() -> "create");

    }

Let’s setup the BeforeEach and AfterEach for this test case.

 @BeforeEach
    public void beforeEach(){
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderType(1L);
        orderEntity.setOrderName("order-1");
        orderEntityRepository.save(orderEntity);
    }
    @AfterEach
    public void afterEach(){
        orderEntityRepository.deleteAll();
    }

Let’s write the test case for the save method

@Test
    void saveOrderEntity() {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderType(1L);
        orderEntity.setOrderName("order-2");
        webTestClient.post()
                .uri("/save")
                .bodyValue(orderEntity)
                .exchange()
                .expectHeader()
                .contentType(MediaType.APPLICATION_JSON)
                .expectStatus()
                .is2xxSuccessful()
                .expectBody(OrderEntity.class)
                .consumeWith(orderentity -> Assertions.assertNotNull(orderentity.getResponseBody().getId()));
    }

And now for get method

  @Test
    void getOrderEntity() {
        webTestClient.get()
                .uri("/allorders")
                .exchange()
                .expectHeader()
                .contentType(MediaType.APPLICATION_JSON)
                .expectStatus()
                .is2xxSuccessful()
                .expectBodyList(OrderEntity.class)
                .consumeWith(listOfObject ->{
                   var list  = listOfObject.getResponseBody();
                    Assertions.assertTrue(list.size() == 1);
                    Assertions.assertTrue(list.get(0).getOrderTypeDes().equals("FullFilled"));
                });
    }

When we run the test cases, the MySQL image will be pulled when running for the first time, and lots of logs will be generated. Test containers have provided recommended logback.xml to get rid of verbosity.

Here is the logback-test.xml that can be included in the resource folder.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="org.testcontainers" level="INFO"/>
    <logger name="com.github.dockerjava" level="WARN"/>
    <logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
</configuration>

We can see that the MySQL test container has started. We can see the Docker whale icon as well.

I like test containers a lot, as I write great test cases that mimic my production environment. I am quite confident when my code goes into production.

Please find the code on my GitHub link: https://github.com/anilgola90/testcontainers

Feel free to ask your questions in the comment section below.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *