Azurite + Testcontainers : comment tester Azure Blob Storage sans nuage

Azure Blob Storage
Category: 
May 18, 2020

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.

https://zappysys.com/images/forums/azure-storage-emulator-install.png

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.

https://miro.medium.com/max/711/1*VgqE7RtIHPQTshTAN676mg.png

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 !

https://dontpaniclabs.com/wp-content/uploads/2020/03/azurite-quick-look.jpg

Testcontainers

https://pbs.twimg.com/profile_images/688699304970694656/IOUXc3V1_400x400.png

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")
    }

}
  1. 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éthode getMappedPort

Tip

Il existe une implémentation de cette librairie pour l’environnement NodeJs :

Tests d’intégration Spring Boot

200

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’application Spring 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 blob blobName du container containerName avec le contenu du body

  • GET /containers/{containerName}/{blobName} : retourne le contenu du blob blobName du container containerName

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")
            }
        }

    }
}
  1. Il faut positionner l’extension @Testcontainers avant @SpringBootTest pour que le conteneur Azurite démarre en premier

  2. Surcharge dynamique de la configuration de l’application Spring Boot en utilisant les informations du conteneur démarré avant

  3. 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 Githubhttps://github.com/lectra-tech/azurite-testcontainers