Spring Boot

TESTCONTIANERS


Spring Datafication 2023. 5. 4. 17:12

The question is how do you match database infrastructure of your test environment with the production one?

db flow

Most commonly developers use in-memory databases like H2 or HSQLDB. But what if you want to test your code against SAME/REAL DATABASE INFRASTRUCTURE IN PRODUCTION?

Amigoscode stop using H2 in memory-database RIGHT NOW Provides a better explanation in code but let's see
how we can do it in practice.

DOWNSIDES OF IN-MEMORY DATABASES

  • Lack of support for some SQL features constrains cause less realistic tests(Reduced reliability)

POSSIBLE SOLUTION TestContainers

You can USE TestContainers library. It allows you to run a real database in a docker container and connect to it from your tests.

testContainers official site

How does testContainers work?

TestContainers uses DOCKER API. PORT exposes to the host machine.

  • Starts a container with the DB.
  • Exposes the PORT of DB.
  • Provides and Connect the JDBC URL.
  • Container is destroyed after tests are finished.

HOW TO USE IT ?

In a simple maven application we can add the following dependency to our pom.xml file:
Based on the database we are using we can add appropriate infrastructure dependency.

<?xml version="1.0" encoding="UTF-8"?>
<project >
    <!--...-->
  <properties> 
      <testcontainers.version>1.17.6</testcontainers.version>
  </properties>
  <dependencies>
      <!--TEST CONTAINERS-->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>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>
        <!--END TEST CONTAINERS--> 
    </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>
</project>

Lets have a look at how we can test our DAO layer with testContainers.

In this instance we would like to test our

public interface UserEntityRepository extends CrudRepository<UserEntity, String> {
}

We can start our TEST by first removing @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE))the default embedded database for testing

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class UserEntityRepositoryTest {
    @Container
    private static GenericContainer<?> container= new PostgreSQLContainer<PostgreSQLContainer>("postgres:13")
            .withExposedPorts(5432)
            .withEnv("POSTGRES_USER", "test")
            .withEnv("POSTGRES_PASSWORD", "test")
            .withEnv("POSTGRES_DB", "test");

    @Test
    public void givenUserName_canSelectByUsername() {

    }

}

The above method is a generic way of starting a container.
We can be more specific by using the following method:
Also we can tell spring to use dynamic properties for the test.


@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class UserEntityRepositoryTest {
    @Container
    private static PostgreSQLContainer<?> container= new PostgreSQLContainer<PostgreSQLContainer>("postgres:13"); 
    @DynamicPropertySource
    static void setUpDB(DynamicPropertyRegistry registry){
        registry.add("spring.datasource.url",container::getJdbcUrl );
        registry.add("spring.datasource.password", container::getPassword);
        registry.add("spring.datasource.username", container::getUsername);
    }

    @Test
    public void givenUserName_canSelectByUsername() {

    }

}

If we check the EXTENDED classes(TestcontainersExtension) of the TestContainers we can see that it has a lot of useful methods.That tracks all the JUNIT life cycle

We can follow the logs by running our test class again.

But then a better log view configuration for the test resource is provided from the official site.

<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"/>
    <!-- The following logger can be used for containers logs since 1.18.0 -->
    <logger name="tc" 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>

Our test class logs looks like below:

testContainerLogs

The Test Passed. But then ?

The test passes, but then do we need to create a new container for each test Class?

Our code

 @Container
    private static PostgreSQLContainer<?> container= new PostgreSQLContainer<>("postgres:13");
    @DynamicPropertySource
    static void setUpDB(DynamicPropertyRegistry registry){
        registry.add("spring.datasource.url",container::getJdbcUrl );
        registry.add("spring.datasource.password", container::getPassword);
        registry.add("spring.datasource.username", container::getUsername);
    }

Would repeat in several classes.

POSSIBLE NOT

We can create an abstract class that would be extended by all the test classes.

@Testcontainers
public abstract class DBAbstractionTest {
    @Container
    private static PostgreSQLContainer<?> container= new PostgreSQLContainer<>("postgres:13");
    @DynamicPropertySource
    static void setUpDB(DynamicPropertyRegistry registry){
        registry.add("spring.datasource.url",container::getJdbcUrl );
        registry.add("spring.datasource.password", container::getPassword);
        registry.add("spring.datasource.username", container::getUsername);
    }
}

And still our test would pass.

A Better Approach

TestContainers provides a way to reuse the same container for all the test classes.

Which provides a profile for the test classes.

And it has specification for each db infrastructure.

In our case we are using PostgreSQLContainer at src/test/resources/application-postgres.properties

spring.datasource.url=jdbc:tc:postgresql:13:///omayfas
spring.test.database.replace=none

By simply adding these properties with our active profile set to postgres
We still get a passed test.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("postgres")
public class UserEntityRepositoryTest {
    @Autowired
    UserEntityRepository userEntityRepository;
    @Test
    @DisplayName("Given USER EXISTS, when findByName, then return UserEntity")
    public void givenIfUserExist_canQueryByName() {
        userEntityRepository.save(new UserEntity("test", "test", "test@gmail.com", "test", "test.png", LoginProvider.APP));
        Optional<UserEntity> test = userEntityRepository.findByName("test");

        Assertions.assertThat(test).hasValueSatisfying(ue -> {
            Assertions.assertThat(ue.getName()).isEqualTo("test");
            Assertions.assertThat(ue.getProvider()).isEqualTo(LoginProvider.APP);
            Assertions.assertThat(ue.getUsername()).isNotNull();
        });

    }

}

ERROR WITH PROFILE

Notice with no profile we get an error.

Because the application test context is not aware of the container.
and fails to resolve the datasource properties for the test classes.
So our application text context should be like.

@SpringBootTest
@ActiveProfiles("postgres")
class OmayfasBlogApplicationTests {

    @Test
    void contextLoads() {
    }

}

## Conclusion

Provides a way to test our application with a real database.

Ability to test our queries and the database itself.

Give it a try.

REFERENCES

반응형