Conteneurs de test - Comment faciliter les tests ?
Bartlomiej Kuczynski
Vous cherchez un moyen de faire des tests plus facilement ? Nous avons ce qu'il vous faut ! Consultez l'article suivant et apprenez comment y parvenir.
Le développement d'applications modernes repose sur une règle simple :
Utiliser la composition
Nous composons des classes, des fonctions et des services pour en faire des éléments logiciels plus importants. Ce dernier élément est à la base des microservices et des architecture hexagonale. Nous aimerions utiliser des solutions existantes, les intégrer à notre logiciel et passer directement à l'étape de la mise en œuvre. marché.
Vous souhaitez gérer l'enregistrement des comptes et stocker les données des utilisateurs ? Vous pouvez choisir l'un des services OAuth. Votre application propose peut-être une forme d'abonnement ou de paiement ? Il existe de nombreux services qui peuvent vous aider à gérer cela. Vous avez besoin d'analyses sur votre site web, mais vous ne comprenez pas le GDPR ? N'hésitez pas à opter pour l'une des solutions prêtes à l'emploi.
Quelque chose qui rend le développement si facile du point de vue de l'entreprise peut vous donner des maux de tête - au moment où vous devez écrire un simple test.
Les bêtes fantastiques : Files d'attente, bases de données et comment les tester
Les tests unitaires sont assez simples. Si vous vous contentez de suivre les règles, votre environnement de test et votre système de gestion de l'information ne seront pas perturbés. code sont saines. Quelles sont ces règles ?
Facile à écrire - un test unitaire doit être facile à écrire car on en écrit beaucoup. Moins d'efforts signifient que plus de tests sont écrits.
Lisible - le code de test doit être facile à lire. Le test est une histoire. Il décrit le comportement du logiciel et peut être utilisé comme raccourci de documentation. Un bon test unitaire vous aide à corriger les bogues sans déboguer le code.
Fiable - le test ne doit échouer que s'il y a un bogue dans le système testé. C'est évident ? Pas toujours. Parfois, les tests réussissent si vous les exécutez un par un, mais échouent lorsque vous les exécutez en tant qu'ensemble. Ils réussissent sur votre machine, mais échouent sur CI (Fonctionne sur ma machine). Un bon test unitaire n'a qu'une seule raison d'échouer.
Rapide - Les tests doivent être rapides. La préparation de l'exécution, le démarrage et l'exécution du test lui-même doivent être très rapides. Sinon, vous les écrirez, mais ne les exécuterez pas. Des tests lents sont synonymes de perte de concentration. Vous attendez et regardez la barre de progression.
Indépendants - enfin, le test doit être indépendant. Cette règle découle des précédentes. Seuls des tests réellement indépendants peuvent constituer une unité. Ils n'interfèrent pas les uns avec les autres, peuvent être exécutés dans n'importe quel ordre et les échecs potentiels ne dépendent pas des résultats des autres tests. Indépendants signifie également qu'ils ne dépendent pas de ressources externes telles que des bases de données, des services de messagerie ou des systèmes de fichiers. Si vous avez besoin de communiquer avec des ressources externes, vous pouvez utiliser des mocks, des stubs ou des dummies.
Tout se complique lorsque nous voulons écrire des tests d'intégration. Ce n'est pas grave si nous voulons tester quelques services ensemble. Mais lorsque nous devons tester des services qui utilisent des ressources externes comme des bases de données ou des services de messagerie, nous nous exposons à des problèmes.
Pour effectuer le test, vous devez installer...
Il y a de nombreuses années, lorsque nous voulions réaliser des tests d'intégration et utiliser, par exemple, des bases de données, nous avions deux options :
Nous pouvons installer une base de données localement. Configurez un schéma et connectez-vous à partir de notre test ;
Nous pouvons nous connecter à une instance existante "quelque part dans l'espace".
Les deux ont des avantages et des inconvénients. Mais tous deux introduisent des niveaux de complexité supplémentaires. Parfois, il s'agissait d'une complexité technique découlant des caractéristiques de certains outils, par exemple l'installation et la gestion d'une base de données Oracle sur votre hôte local. Parfois, il s'agissait d'un inconvénient dans le processus, par exemple, vous devez être d'accord avec le test. équipe sur l'utilisation de JMS... à chaque fois que vous voulez exécuter des tests.
Les conteneurs à la rescousse
Au cours des dix dernières années, l'idée de la conteneurisation a gagné en reconnaissance dans l'industrie. Il est donc naturel de choisir les conteneurs comme solution à notre problème de test d'intégration. Il s'agit d'une solution simple et propre. Il suffit d'exécuter le processus de construction et tout fonctionne ! Vous n'arrivez pas à y croire ? Jetez un coup d'œil à cette configuration simple d'un build maven :
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
test-compile
up
${projet.basedir}/docker-compose.yml
true
down
post-intégration-test
down
${projet.basedir}/docker-compose.yml
true
Et le docker-compose.yml Le dossier a l'air plutôt bien, lui aussi !
L'exemple ci-dessus est très simple. Une base de données postgres, pgAdmin et c'est tout. Lorsque vous exécutez
bash
$ mvn clean verify
puis le plugin maven démarre les conteneurs et les désactive après les tests. Les problèmes commencent lorsque le projet grandit et que notre fichier de composition grandit aussi. À chaque fois, vous devrez démarrer tous les conteneurs, et ils resteront en vie pendant toute la durée de la construction. Vous pouvez améliorer un peu la situation en changeant la configuration de l'exécution du plugin, mais ce n'est pas suffisant. Dans le pire des cas, vos conteneurs épuisent les ressources du système avant que les tests ne commencent !
Et ce n'est pas le seul problème. Vous ne pouvez pas exécuter un seul test d'intégration à partir de votre IDE. Avant cela, vous devez démarrer les conteneurs à la main. De plus, la prochaine exécution de maven démolira ces conteneurs (jetez un coup d'œil à vers le bas exécution).
Cette solution est donc comme un gros cargo. Si tout fonctionne bien, tout va bien. Tout comportement inattendu ou inhabituel nous conduit à une sorte de désastre.
Conteneurs de test - exécuter des conteneurs à partir de tests
Mais que se passerait-il si nous pouvions faire fonctionner nos conteneurs à partir de tests ? L'idée semble bonne, et elle est déjà en cours de mise en œuvre. Conteneurs d'essaiPuisque nous parlons de ce projet, voici une solution à nos problèmes. Elle n'est pas idéale, mais personne n'est parfait.
Il s'agit d'un Java qui supporte les tests JUnit et Spock, fournissant des moyens légers et faciles d'exécuter le conteneur Docker. Jetons-y un coup d'œil et écrivons un peu de code !
Conditions préalables et configuration
Avant de commencer, nous devons vérifier notre configuration. Conteneurs d'essai besoin :
Docker dans la version v17.09,
Java version 1.8 minimum,
Accès au réseau, en particulier à docker.hub.
Pour en savoir plus sur les exigences relatives à des systèmes d'exploitation et à des systèmes d'information spécifiques, voir en la documentation.
Il est maintenant temps d'ajouter quelques lignes à pom.xml.
org.testcontainers
testcontainers-bom
${testcontaines.version}
pom
import
org.postgresql
postgresql
runtime
org.testcontainers
postgresql
test
org.testcontainers
junit-jupiter
test
J'utilise Conteneurs d'essai version 1.17.3mais n'hésitez pas à utiliser la plus récente.
Tests avec le conteneur Postgres
La première étape consiste à préparer notre instance de conteneur. Vous pouvez le faire directement dans le test, mais il est préférable d'utiliser une classe indépendante.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC() ;
private Postgres13TC() {
super("postgres:13.2") ;
}
public static Postgres13TC getInstance() {
return TC ;
}
@Override
public void start() {
super.start() ;
System.setProperty("DB_URL", TC.getJdbcUrl()) ;
System.setProperty("DB_USERNAME", TC.getUsername()) ;
System.setProperty("DB_PASSWORD", TC.getPassword()) ;
}
@Override
public void stop() {
// ne fait rien. Il s'agit d'une instance partagée. Laissez la JVM gérer cette opération.
}
}
Au début des tests, nous créerons une instance de Postgres13TC. Cette classe permet de gérer les informations relatives à notre conteneur. Les plus importantes sont les chaînes de connexion à la base de données et les informations d'identification. Il est maintenant temps d'écrire un test très simple.
J'utilise ici JUnit 5. Annotation @Testcontainers fait partie des extensions qui contrôlent les conteneurs dans l'environnement de test. Ils trouvent tous les champs avec @Container et les conteneurs de démarrage et d'arrêt respectivement.
Tests avec Spring Boot
Comme je l'ai déjà mentionné, j'utilise Spring Boot dans le projet. Dans ce cas, nous devons écrire un peu plus de code. La première étape consiste à créer une classe de configuration supplémentaire.
Cette classe remplace les propriétés existantes par des valeurs provenant de la classe récipient d'essai. Les trois premières propriétés sont des propriétés standard de Spring. Les cinq suivantes sont des propriétés supplémentaires, personnalisées, qui peuvent être utilisées pour configurer d'autres ressources et extensions comme liquibase, par exemple :
Il est maintenant temps de définir un test d'intégration simple.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initializers = ContainerInit.class)
@Testcontainers
class DummyRepositoryTest {
@Autowired
private DummyRepository ;
@Test
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L) ;
var expected = new Dummy() ;
expected.setId(10L) ;
assertThat(byId).completes().emitsCount(1).emits(expected) ;
}
}
Nous avons ici quelques annotations supplémentaires.
@SpringBootTest(webEnvironment = RANDOM_PORT) - marque le test comme un test Spring Boot et démarre le contexte Spring.
@AutoConfigureTestDatabase(replace = NONE) - Ces annotations indiquent que l'extension Spring Test ne doit pas remplacer la configuration de la base de données postgres par H2 dans la configuration de la mémoire.
@ContextConfiguration(initializers = ContainerInit.class) - un contexte de printemps supplémentaire où nous mettons en place des propriétés de Conteneurs d'essai.
@Testcontainers - comme indiqué précédemment, cette annotation contrôle le cycle de vie du conteneur.
Dans cet exemple, j'utilise des référentiels réactifs, mais cela fonctionne de la même manière avec les référentiels JDBC et JPA courants.
Nous pouvons maintenant exécuter ce test. S'il s'agit de la première exécution, le moteur doit extraire les images de docker.hub. Cela peut prendre un moment. Après cela, nous verrons que deux conteneurs ont été exécutés. L'un est postgres et l'autre est Testcontainers controller. Ce deuxième conteneur gère les conteneurs en cours d'exécution et même si la JVM s'arrête inopinément, il éteint les conteneurs et nettoie l'environnement.
En résumé
Conteneurs d'essai sont des outils très faciles à utiliser qui nous aident à créer des tests d'intégration utilisant des conteneurs Docker. Cela nous donne plus de flexibilité et augmente la vitesse de développement. La mise en place correcte de la configuration des tests réduit le temps nécessaire à l'intégration des nouveaux développeurs. Ils n'ont pas besoin de configurer toutes les dépendances, il suffit d'exécuter les tests écrits avec les fichiers de configuration sélectionnés.