Skip to content

TestContainer

GradedJestRisk edited this page Sep 25, 2024 · 1 revision

Test container

Why ?

Automated database follow these steps:

  • create a container (manually or automatically) ;
  • create a schema (using Model using Hibernate, or using DB version control, eg. Liquibase) ;
  • cleaning table to be used ;
  • insert data (test given) ;
  • execute SUT ;
  • read data back ;
  • assert.

Time is consumed in :

  • starting container ;
  • creating schema;
  • performing DML.

Time can be saved if, for several tests :

  • the container is started only once;
  • schema is created only once;
  • DML queries are fast.

In-memory database speed up things in:

  • starting quickly;
  • executing DML fast, not using fs.

If using testcontainers :

  • container creation is fast (< 5 seconds), but you may create it several times ;
  • DML will be faster than using out-of-the-box PostgreSQL image because fs is not used, with fysnc=off option;
  • but if you can create schema once, you can save much time.

So, if you want quick tests, you'll have to implement kind of singleton. This may be quite complex, cause Hibernate schema creation from model hbm2ddl cannot be used programmatically. You'll have to trigger your DB versioning tool, which is slower. But if you do it only once, and run many tests in your workday, it may be a good bet.

2 solutions

2 ways:

  • modify connexion URL (eg. jdbc) - doc
  • JUnit
    • Junit4 : use rule doc
    • Junit 5 : use extensions doc

With jdbc (throwaway)

Modify URL

In test/resources/application.yml

spring:
  first-datasource:
    url: jdbc:tc:postgresql:16:///first
  second-datasource:
    url: jdbc:tc:postgresql:16:///second

You can add options, like :

spring:
  first-datasource:
    url: jdbc:tc:postgresql:16:///first?TC_REUSABLE=true?TC_TMPFS=/testtmpfs:rw

Reuse: check tile it takes to start up container using logging.

Connect

You can connect using

# get port
docker ps 
psql --dbname "host=localhost port=$PORT user=test password=test dbname=postgres"

With JUnit and Spring (singleton)

Example with two datasources, with all tests sharing the same instance.

Add libraries

testImplementation "org.testcontainers:testcontainers"
testImplementation "org.testcontainers:postgresql"
testImplementation "org.testcontainers:junit-jupiter"

Configure spring

In test/resources/application.yml

Remove all datasources

spring

Deactivate liquibase, as it will look for a datasource at startup if active

spring
    liquibase:
        enabled: false

Use extension

Create extension to start containers and tell Spring to use them as datasource

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.PostgreSQLContainer;

public class StartDatabaseAndCreateSchema implements BeforeAllCallback {

  @Override
  public void beforeAll(ExtensionContext context) throws LiquibaseException, SQLException {

    PostgreSQLContainer firstContainer =
        (PostgreSQLContainer)
            new PostgreSQLContainer("postgres:16-alpine")
                .withDatabaseName("first")
                .withUsername("user")
                .withPassword("password")
                .withExposedPorts(5432)
                .withReuse(true);

    PostgreSQLContainer secondContainer =
        (PostgreSQLContainer)
            new PostgreSQLContainer("postgres:16-alpine")
                .withDatabaseName("second")
                .withUsername("user")
                .withPassword("password")
                .withExposedPorts(5432)
                .withReuse(true);

    firstContainer.start();
    secondContainer.start();
    updateDataSourceProps("first-datasource", firstContainer);
    updateDataSourceProps("second-datasource", secondContainer);

    System.out.println("First database: ");
    System.out.println("Port is " + firstContainer.getMappedPort(5432));
    System.out.println("URL is " + firstContainer.getJdbcUrl());

    System.out.println("Second database: ");
    System.out.println("Port is " + secondContainer.getMappedPort(5432));
    System.out.println("URL is " + secondContainer.getJdbcUrl());

    System.out.println("Databases ready");
  }

  private void updateDataSourceProps(String name, PostgreSQLContainer container) {

    System.setProperty("spring." + name + ".url", container.getJdbcUrl());
    System.setProperty("spring." + name + ".username", container.getUsername());
    System.setProperty("spring." + name + ".password", container.getPassword());
  }
}

And then use the extension in test

import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@ExtendWith({StartDatabaseAndCreateSchema.class})
class TestContainerTest extends BaseTestIntegration {

  @Autowired private Repository repository;

  @BeforeEach
  void setUp() {
    repository.deleteAll();
  }

  @Test
  @DisplayName("#findAll")
  void findAll() {
    // Given
    repository.saveAll(List.of(<A_RECORD>);

    // When
    List<RECORD> actual = repository.findAll();)

    // Then
    assertThat(actual)
        .singleElement()
        .usingRecursiveComparison()
        .isEqualTo(<A_RECORD>);
  }
}

Connect

Given this configuration

   PostgreSQLContainer permissionPostgreSQLContainer =
        (PostgreSQLContainer)
            new PostgreSQLContainer("postgres:16-alpine")
                .withDatabaseName("permission")
                .withUsername("user")
                .withPassword("password")
                .withExposedPorts(5432)
                .withReuse(true);

Log the port at runtime

System.out.println("Port is " + permissionPostgreSQLContainer.getMappedPort(5432));
System.out.println("URL is " + permissionPostgreSQLContainer.getJdbcUrl());

Or use docker ps

Then connect

psql postgresql://user:password@localhost:$PORT/trace

Logging

    <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"/>

https://java.testcontainers.org/supported_docker_environment/logging_config/

Misc

Singleton https://java.testcontainers.org/test_framework_integration/manual_lifecycle_control/

With spring boot

https://github.com/bedla/spring-boot-postgres-testcontainers/tree/master

Ports https://java.testcontainers.org/features/networking/

Clone this wiki locally