Testing Spring Boot Applications with Testcontainers and Oracle

Testing Spring Boot Applications with Testcontainers and Oracle

At Spring One in Las Vegas a couple of months ago, I learned about Testcontainers and the expanded support for using them in Spring Boot 3, and I thought they were great! I love testing! So I could not wait to try them out.

In this article, I want to show you how to easily test your Spring Boot applications with a full Oracle 23c database, with only about a 10 second startup time, which is fast enough to use even in unit tests.

Let's create a Spring Boot 3 application that uses JPA and has a simple REST API. Here's the POM file for the application:

<?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.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>demo</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.oracle.database.spring</groupId>
            <artifactId>oracle-spring-boot-starter-ucp</artifactId>
            <type>pom</type>
            <version>3.1.0</version>
        </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>oracle-free</artifactId>
            <version>1.19.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>        

It's a pretty standard project, but I've added a couple of extra dependencies, shown again below, the first one is to enable testcontainers itself, and the second is for the Oracle 23c Free database testcontainer:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>oracle-free</artifactId>
    <version>1.19.2</version>
    <scope>test</scope>
</dependency>        

Now, let's write the test! Create a new file called DemoApplicationTests.java in src/test/java/com/example/demo. We are going to want two annotations on this class:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)        

The first one enables testcontainers for this test class. Since we are going to run these tests with a real database, we will also run the full application context and the web server (as opposed to using something like @WebMvcTest and MockMvc (which, by the way, are also a great way to write tests without needing a full stack, but that's a subject for another article!). To avoid port conflicts, we tell SpringBootTest to use a random port. We can get the port by injecting a value into the class:

@Value(value = "${local.server.port}")
private int port;        

Now, let's handle the container configuration:

@Container
@ServiceConnection
static OracleContainer oracleContainer = new OracleContainer(
    DockerImageName.parse("gvenzl/oracle-free:slim-faststart")
            .asCompatibleSubstituteFor("gvenzl/oracle-free"))
        .withDatabaseName("pdb1")
        .withUsername("testuser")
        .withPassword(("testpwd"));        

I overrided the image because I wanted to use the slim-faststart variant, which (as the name suggests) is a bit smaller and is optimized to start faster, so its a good choice for testing. Then I just configured the database name and a user for the tests to use.

Great, now let's write the actual test:

private static final ObjectMapper mapper = new ObjectMapper();

@Autowired
AnimalRepository animalRepository;

@Test
void testGetAnimals() {
    // create the test data
    List<Animal> animals = List.of(
        new Animal(null, "cat"),
        new Animal(null, "dog")
    );
    animalRepository.saveAll(animals);

    // check the api
    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<Object[]> result = restTemplate.getForEntity(
        "http://127.0.0.1:" + port + "/animals", Object[].class
    );
    Object[] objects = result.getBody();

    List<String> actualAnimals = Arrays.stream(objects)
        .map(object -> mapper.convertValue(object, Animal.class))
        .map(Animal::getName)
        .collect(Collectors.toList());

    assertTrue(actualAnimals.containsAll(Arrays.asList("cat", "dog")),
        "The expected animals should be present");
}        

First, I have injected an ObjectMapper so we can convert the JSON response into my Animal POJO. And I've injected the JPA repository.

Then in the actual test method, I start by creating some test data (a cat and a dog) and saving them in the database.

Next, I am calling the actual REST API, using the random port we injected earlier, and grabbing the results into an Object[].

Next, I am converting those Objects into Animals and extracting the name of each one into a new list.

Finally, I am asserting that the list of animals that I got back contains the two that I expect to be there. Note that I am not assuming it contains only those two, because some other test might run in parallel and create some other data in the repository. I could annotate my test with @DirtiesContext so I don't have to worry about other tests, but then I'd have to restart the database container each time and that would slow everything down, so I prefer to write the test with less assumptions instead.

So here's the finished test class:

package com.example.demo;

import com.example.demo.model.Animal;
import com.example.demo.repository.AnimalRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.oracle.OracleContainer;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
import org.testcontainers.utility.DockerImageName;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertTrue;

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

    @Value(value = "${local.server.port}")
    private int port;

    private static final ObjectMapper mapper = new ObjectMapper();

    @Container
    @ServiceConnection
    static OracleContainer oracleContainer = new OracleContainer(
            DockerImageName.parse("gvenzl/oracle-free:slim-faststart")
                    .asCompatibleSubstituteFor("gvenzl/oracle-free"))
                .withDatabaseName("pdb1")
                .withUsername("testuser")
                .withPassword(("testpwd"));

    @Autowired
    AnimalRepository animalRepository;

    @Test
    void testGetAnimals() {
        // create the test data
        List<Animal> animals = List.of(
                new Animal(null, "cat"),
                new Animal(null, "dog")
        );
        animalRepository.saveAll(animals);

        // check the api
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Object[]> result = restTemplate.getForEntity(
                "http://127.0.0.1:" + port + "/animals", Object[].class
        );
        Object[] objects = result.getBody();

        List<String> actualAnimals = Arrays.stream(objects)
                .map(object -> mapper.convertValue(object, Animal.class))
                .map(Animal::getName)
                .collect(Collectors.toList());

        assertTrue(actualAnimals.containsAll(Arrays.asList("cat", "dog")),
                "The expected animals should be present");
    }

}        

Of course, it won't run yet because we have not written the application code, so let's do that next.

In src/main/java/com/example/demo, create a file called DemoApplication.java with this content, a very standard Spring Boot application class:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}        

In src/main/java/com/example/demo/model, create a file called Animal.java to hold the model POJO, here's the content, again its very standard, nothing special:

package com.example.demo.model;

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

@Entity
public class Animal {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;
    String name;

    public Animal(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Animal() {}

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}        

For the JPA repository, create a file called AnimalRepository.java in src/main/java/com/example/demo/repository with this content, again there's nothing special here:

package com.example.demo.repository;

import com.example.demo.model.Animal;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AnimalRepository extends JpaRepository<Animal, Long> {
}        

And here's the REST Controller, this goes in a file called AnimalController.java in src/main/java/com/example/demo/controller and its also very standard:

package com.example.demo.controller;

import com.example.demo.model.Animal;
import com.example.demo.repository.AnimalRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AnimalController {

    AnimalRepository animalRepository;

    AnimalController(AnimalRepository animalRepository) {
        this.animalRepository = animalRepository;
    }

    @GetMapping("/animals")
    public ResponseEntity<List<Animal>> getAnimals() {
        return ResponseEntity.ok(animalRepository.findAll());
    }
}        

Finally, we need the JPA configuration in src/main/resources/application.yaml:

spring:
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.OracleDialect
        format_sql: true
    show-sql: true        

Now, we can run the test and see the magic happen!

I hope you agree that that is pretty awesome! Thanks for reading and happy testing!

To view or add a comment, sign in

More articles by Mark Nelson

Others also viewed

Explore content categories