liksi logo

Isolez vos tests sans compromis avec Testcontainers

Jean-François Chalony - Publié le 12/11/2020
Isolez vos tests sans compromis avec Testcontainers

Si les tests d’intégration dans une architecture micro-services vous semblent complexes à mettre en œuvre (jeu de données, dépendance externe, …) alors Testcontainers est sûrement fait pour vous ! Dans ce billet, je vais tâcher de vous montrer comment, grâce à Testcontainers, on peut facilement tester l’intégration d’un service dans son environnement cible avec toutes ses dépendances, j’illustrerai mes propos avec un cas simple d’une API avec une BDD.

Petit rappel

Dans une architecture micro-services le test d’intégration automatisé est important, voire indispensable, pour vérifier l’intégration d’un micro-service dans son environnement tant les dépendances peuvent être nombreuses : interactions avec d’autres micro-services, sources de données, brokers de messages, etc.

Fonctionnalités

Pour pouvoir utiliser Testcontainers dans un projet, il faut a minima ajouter la dépendance vers la librairie dite core (ici exemple via Maven).

<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>testcontainers</artifactId>
   <version>1.14.3</version>
   <scope>test</scope>
</dependency>

Avec cette dépendance, il est désormais possible de créer un conteneur générique Testcontainers à partir d’une image docker

private static final GenericContainer POSTGRE_SQL_TEST_CONTAINER =
          new GenericContainer("postgres:13")
                .withExposedPorts(5432);

Cette commande permet de créer un conteneur générique à partir d’une image docker, mais il existe des test-containers plus spécifiques pour pouvoir construire directement un conteneur PostgreSQL par exemple. Pour cela, il suffit d’ajouter la  dépendance correspondante.

Voir sur maven central les containers existants: https://mvnrepository.com/artifact/org.testcontainers .

private static final PostgreSQLContainer POSTGRE_SQL_TEST_CONTAINER = new PostgreSQLContainer()

En utilisant ces containers spécifiques, ils sont directement paramétrés selon les caractéristiques de l’image Docker cible (port par défaut exposé, login, mot de passe, …).
Dans notre exemple, cela nous permet d’avoir une base PostgreSQL conteneurisée rapidement pour réaliser des tests d’intégration avec un jeu de données maîtrisé.

Mise en place d’un test dans une application springboot

Maintenant, nous allons intégrer l’utilisation de Testcontainers au sein de nos tests d’intégration dans une application Java Springboot.

Dans un premier temps, il va falloir que l’application Springboot utilise la BDD exposée par le conteneur. Pour faire cela, Il faut surcharger les variable “spring.datasource.*” qui permettent de définir la configuration de la connexion à la BDD. L’API de test container met à disposition un ensemble de méthodes permettant d’avoir le contexte d’exécution du conteneur.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Testcontainers
@ContextConfiguration(initializers = {MonIT.Initializer.class})
class MonIT {

    @Container
    private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer();

    @BeforeEach
    void setUp() {
        assertThat(POSTGRE_SQL_CONTAINER.isRunning()).isTrue();
    }

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {

            List<String> pairs = List.of(
                    "spring.datasource.url=" + POSTGRE_SQL_CONTAINER.getJdbcUrl(),
                    "spring.datasource.username=" + POSTGRE_SQL_CONTAINER.getUsername(),
                    "spring.datasource.password=" + POSTGRE_SQL_CONTAINER.getPassword());
            TestPropertyValues.of(pairs)
                    .applyTo(configurableApplicationContext.getEnvironment());

        }
    }
}

Écriture d’un test d’intégration

Une fois que tout est configuré et instancié, nous allons prendre pour exemple une simple application du type webservice CRUD. Nous allons prendre pour exemple un GET sur une ressource qui est stockée en BDD.  Je ne vais pas décrire comment créer un WebService avec Springboot, ce n’est pas le but de cet article, je vais directement passer à l’écriture du test.

Et voici comment faire :

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ContextConfiguration(initializers = {MonIT.Initializer.class})
class MonIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void should_get_object() throws Exception {
        final var guid = UUID.randomUUID();
        mockMvc
                .perform(get("/api/v1.0/resources/" + guid)
                        .header("monHeader", "test"))
                .andDo(print())
                .andExpect(status().isOk());
    }
}

Ce premier test d’intégration est simple. En utilisant l’outil de test Spring MockMVC, le code :

  • Lance une requête Http sur le service
  • Teste en retour si le status Http est 200

Le fait d’utiliser un Testcontainer PostgresSQL, nous donne également la possibilité de passer un script SQL d’initialisation au conteneur :

@Container
private static final var POSTGRE_SQL_CONTAINER = new PostgreSQLContainer()
                            .withInitScript("script.sql");

Il est évidemment aussi possible de peupler autrement la base de données via les repository spring-data, par flyway etc.

Aller plus loin

Vous allez me dire que dans ce cas précis, il y a des alternatives à Testcontainers qui permettent d’avoir le résultat aussi facilement. Effectivement vous avez raison, il est, par exemple, possible d’utiliser une embedded database pour pouvoir intégrer votre application avec une BDD.

Mais j’utilise aussi Testcontainer, dans d’autres cas où les alternatives sont plus limitées comme l’intégration d’une application utilisant des queues RabbitMQ en tant que consommateur de messages ou sur des exchanges RabbitMQ en tant qu’émetteur. Dans ce cas, il est intéressant de pouvoir avoir une instance de RabbitMQ dédiée aux tests d’intégrations afin d’isoler son environnement de tests. En effet, dans le cas d’une utilisation d’un RabbitMQ d’une plateforme de tests ou d’intégration, il est fréquent de se faire polluer par l’exécution d’autres tests en parallèle et donc une isolation complète du test est préférable.

Appriéciez-donc la simplicité de mise en place : ajout de la dépendance Maven et la création du conteneur

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>rabbitmq</artifactId>
  <scope>test</scope>
</dependency>
@Container
private static final RabbitMQContainer RABBIT_MQ_CONTAINER = new RabbitMQContainer();

Voila vous avez un RabbitMQ prêt pour vos tests d’intégrations. Il possible de créer vos queues, bindings et exchange à l’initialisation de votre Testcontainer.

Voici un exemple de tests d’intégration, sur une application qui écoute une queue et qui émet en résultat un message sur exchange. Pour tester le résultat dans la queue, j’utilise Awaitility qui permet de tester une méthode asynchrone.

@Container
private static final RabbitMQContainer RABBIT_MQ_CONTAINER = new RabbitMQContainer()
            .withQueue("myQueue")
            .withQueue("myQueueOut")
            .withExchange("exchangeIn", ExchangeTypes.FANOUT)
            .withExchange("exchangeOut", ExchangeTypes.FANOUT)
            .withBinding("exchangeIn", "myQueue", Collections.emptyMap(), "routing.myqueue", "queue")
            .withBinding("exchangeOut", "myQueueOut");

@Autowired
private RabbitMessagingTemplate rabbitTemplate;

@BeforeEach
void setUp() {
  assertThat(RABBIT_MQ_CONTAINER.isRunning()).isTrue();
}

@Test
void should_test_queue() throws InterruptedException {
  rabbitTemplate.convertAndSend("exchangeIn", "routing.myqueue", "myMessage".getBytes());

  Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> rabbitTemplate.receiveAndConvert("myQueueOut", String.class), Predicate.isEqual("OK"));
}

Bilan

Après la lecture de cet article, que vous soyez conquis ou non par l’utilisation de Testcontainer dans la construction de vos tests d’intégration, j’espère vous avoir convaincu de la simplicité et la rapidité d’utilisation de cette librairie de test. La sortie de la version 1.15.0 du framework est une bonne occasion de le mettre son nez dedans.

Maintenant à vous de jouer !

Liens

Derniers articles