Contexte
Pour le développement de l’offre Cloud Lectra, nous utilisons Microsoft Azure
comme plateforme Cloud, et en particulier Azure Blob Storage
pour le stockage de fichier binaire.
Microsoft fournit un SDK pour les langages sur la JVM (Kotlin
pour nous) : https://github.com/Azure/azure-sdk-for-java
avec un module pour l’accès à l’API de Blob Storage
:
<dependency> <groupId>com.azure</groupId> <artifactId>azure-storage-blob</artifactId> <version>12.6.1</version> </dependency>
En phase de développement, nous essayons au maximum de s’isoler des ressources Cloud, comme si nous étions déconnectés du réseau. Cette pratique permet d’avoir un environnement de développement plus stable, plus rapide et moins cher (sic). En plus, nous utilisons cette approche pour permettre une meilleure testabilité, en s’intégrant dans nos tests de composants (1). Cependant, il ne faut pas négliger les tests d’intégration avec l’utilisation réelle des ressources Cloud (2).
Emulateur Azure Blob Storage
Microsoft fournit un outil qui émule les services de stockage de Azure afin de pouvoir tester localement son application sans avoir à souscrire d’abonnement au service. L’émulateur de stockage repose sur l’utilisation d’une instance de Microsoft SQL Server Express LocalDB … et tout ça uniquement sous Windows. A Lectra, même si nous avons majoritairement un parc de machine de développement sous Windows, il est possible d’utiliser un environnement de développement sous Linux ou Mac.
De plus, cet émulateur est difficile à intégrer dans une suite de tests automatisés.
Azurite
Historique
Etant sous environnement de dévoppement MacOS, j’ai cherché des solutions alternatives à cet émulateur. Il y a 3 ans, j’ai trouvé un projet open source Azurite
(https://github.com/arafato/azurite) qui a commencé le développement d’un autre émulateur basé cette fois-ci sur un serveur NodeJS
. Cette technologie permet de l’utiliser sur toutes les plateformes.
De plus, j’ai proposé au créateur de publier une image Docker (https://github.com/arafato/azurite/issues/77) et hop quelques jours plus tard l’image était publiée dans le Docker Hub (https://hub.docker.com/r/arafato/azurite). A partir de là, il était beaucoup plus facile pour moi d’utiliser localement les APIs de Azure Blob Storage et de pouvoir l’intégrer à des tests automatisés.
Microsoft
En mai 2018, Microsoft a trouvé ce projet très intéressant et a proposé son support. Le projet a été déplacé dans le repository Github de Azure
(3) et le développement a continué pour supporter de plus en plus de fonctionnalités de Azure Storage. Aujourd’hui, le projet est même intégré à la documentation générale → https://docs.microsoft.com/fr-fr/azure/storage/common/storage-use-azurite?toc=/azure/storage/blobs/toc.json
De même une image officielle a été publiée dans le Docker Hub : https://hub.docker.com/_/microsoft-azure-storage-azurite
docker pull mcr.microsoft.com/azure-storage/azurite
Une extension Visual Studio Code
a même aussi été développée !
Testcontainers
Testcontainers (4) est une librairie Java
qui facilite l’utilisation d’image Docker dans des tests JUnit
. Le seul pré-requis est que Docker
soit installé (https://www.testcontainers.org/supported_docker_environment/).
Grâce à une Rule
pour JUnit 4
ou une Extension
pour JUnit 5
, la librairie se charge de récupérer une image Docker
donnée et de démarrer un conteneur en proposant un DSL pour paramétrer toutes les options. Ensuite, il suffit au développeur d'écrire son test qui va pouvoir utiliser la ressource démarrée. Les cas d’utilisation les plus courants sont les bases de données, ou comme dans notre contexte des émulateurs de service Cloud.
Voici un exemple en utilisant l’image Docker Azurite
:
class AzuriteContainer : GenericContainer<AzuriteContainer>("mcr.microsoft.com/azure-storage/azurite") @Testcontainers class AzuriteTest : WithAssertions { companion object { // will be shared between test methods @Container private val azuriteContainer = AzuriteContainer().withExposedPorts(10000) } // Azurite default configuration private val defaultEndpointsProtocol = "http" private val accountName = "devstoreaccount1" private val accountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" private val blobEndpoint = "http://127.0.0.1:${azuriteContainer.getMappedPort(10000)}/devstoreaccount1" private val connectionString = "DefaultEndpointsProtocol=$defaultEndpointsProtocol;AccountName=$accountName;AccountKey=$accountKey;BlobEndpoint=$blobEndpoint;" //<1> @Test fun `check container is running`() { assertThat(azuriteContainer.isRunning).isTrue() } @Test fun `should upload and download blob`() { // create a container for blob val containerName = "container-" + UUID.randomUUID() val containerClient = BlobContainerClientBuilder() .connectionString(connectionString) .containerName(containerName) .buildClient() containerClient.create() // create blob and upload text val blobName = "blob-" + UUID.randomUUID() val blobClient = containerClient.getBlobClient(blobName) val content = "hello world".toByteArray() blobClient.upload(ByteArrayInputStream(content), content.size.toLong()) // read text of blob val text = blobClient.openInputStream().use { String(it.readAllBytes()) } assertThat(text).isEqualTo("hello world") } }
-
Testcontainers
utilise des ports aléatoires pour chaque conteneur qu’il démarre, et il est possible de récupérer la valeur de ce port avec la méthodegetMappedPort
Tip
|
Il existe une implémentation de cette librairie pour l’environnement |
Tests d’intégration Spring Boot
Notre stack technique préférée pour le développement de nos microservices est : Spring Boot + Kotlin
. Spring Boot
propose un module pour faciliter l’intégration avec JUnit
.
Par exemple, il est possible d'écrire des tests de composants qui permettent de lancer l’application pour pouvoir faire des tests en mode boîte noire (5).
@SpringBootTest class ControllerTest(@Autowired private val wac: WebApplicationContext) { private val mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() @Test fun `ping should respond pong`() { mockMvc.get("/ping").andExpect { status { isOk } content { string("pong") } } } }
Dans cette perspective, nous pouvons utiliser Testcontainers
pour démarrer tous les services externes nécessaires à l’application, comme par exemple un émulateur Azurite
;-)
Cependant, nous avons été confrontés à des problèmes de dépendances entre l’application et le conteneur démarré par Testcontainers
:
-
d’abord il faut que le conteneur
Azurite
démarre avant l’application Spring Boot -
puis, comme
Testcontainers
utilise des ports dynamiques pour le container, il faut modifier la configuration de l’applicationSpring Boot
pour qu’elle prenne en compte cette configuration dynamique.
Pour résoudre ce problème, il suffit de bien ordonner les extensions JUnit 5
puis de surcharger la configuration Spring
avant le démarrage de l’application grâce à l’annotation @DynamicPropertySource
. La documentation Spring
vient d'être mise à jour pour décrire la solution : https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-testcontainers
Exemple
Imaginons que notre application SpringBoot
propose l’API REST suivante :
-
PUT /containers/{containerName}/{blobName}
: crée ou met à jour le blobblobName
du containercontainerName
avec le contenu du body -
GET /containers/{containerName}/{blobName}
: retourne le contenu du blobblobName
du containercontainerName
Voici le test de composant que l’on peut écrire en utilisant SpringBootTest
et Testcontainers
:
@Testcontainers //<1> @SpringBootTest class ApplicationTest(@Autowired private val wac: WebApplicationContext) { private val mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() companion object { @Container private val azuriteContainer = AzuriteContainer().withExposedPorts(10000) @DynamicPropertySource //<2> @JvmStatic //<3> fun configureApplication(registry: DynamicPropertyRegistry) { registry.add("azure.storage.blob-endpoint") { "http://127.0.0.1:${azuriteContainer.getMappedPort(10000)}/devstoreaccount1" } } } @Test fun `should put and get blob content`() { val containerName = "container-" + UUID.randomUUID() val blobName = "blob-" + UUID.randomUUID() mockMvc.put("/containers/${containerName}/${blobName}") { content = "Hello Azure" contentType = MediaType.TEXT_PLAIN }.andExpect { status { isOk } } mockMvc.get("/containers/${containerName}/${blobName}").andExpect { status { isOk } content { string("Hello Azure") } } } }
-
Il faut positionner l’extension
@Testcontainers
avant@SpringBootTest
pour que le conteneurAzurite
démarre en premier -
Surcharge dynamique de la configuration de l’application Spring Boot en utilisant les informations du conteneur démarré avant
-
La méthode portant l’annotation
@DynamicPropertySource
doit être statique, d’où l’annotation Kotlin@JvmStatic
permettant de rendre compatible le bytecode généré
Conclusion
En combinant SpringBoot
et Testcontainers
, nous pouvons utiliser l'émulateur Azurite
qui nous permet d’avoir une suite de tests automatisés rapides et fiables pour valider l’utilisation des APIs du SDK de Azure Blob Storage.
Well Done !
Vous trouverez un exemple de projet avec les sources des exemples et de l’extension JUnit 5
dans le repository Github
→ https://github.com/lectra-tech/azurite-testcontainers
Ressources
-
Test de composants : https://martinfowler.com/articles/microservice-testing/#testing-component-in-process-diagram
-
Test d’intégration : https://martinfowler.com/articles/microservice-testing/#testing-integration-diagram
-
Azurite : https://github.com/azure/azurite
-
Testcontainers : https://www.testcontainers.org/
-
Black Box Testing : https://en.wikipedia.org/wiki/Black-box_testing