Quand on parle de tests de non régression on pense souvent aux tests côté UI. Selenium, Cucumber, Cypress ou encore TestCafé, les outils ne manquent pas !

Lorsqu’on souhaite réaliser ce type de tests sur des API, le choix est plus restreint mais on parvient tout de même à trouver chaussure à notre pied. Citons à titre d’exemple Postman qui, au-delà d’offrir une interface intuitive facilitant la réalisation d’appels aux endpoints de nos applications, permet de créer de véritables scénarios (retrouvez d’ailleurs notre dernier article sur Postman ici).

Mais il en existe un, beaucoup moins connu, qui offre des fonctionnalités similaires (et plus encore…) avec une approche différente : Karate.
Après avoir comparé cette solution avec quelques leaders du marché, nous verrons quels sont les principaux atouts de Karate et comment l’utiliser.

Alors enfilez votre kimono, ça va tester !

Quelques champions dans le Dõjõ …

Selenium et Cucumber

Cette première méthode consiste a coupler le Webdriver Selenium avec Cucumber. Pas d’interface permettant d’écrire nos campagnes dans le cas présent : le développeur doit créer une série de fichiers en Gherkin dans lesquels seront décrites chaque feature.

Ces features peuvent être vues comme des regroupements de scénarios, où chaque ligne correspond à une action ou vérification qu’il faut ensuite traduire en Java.

Le Gherkin est un langage facilement exploitable par les développeurs, mais aussi par les autres membres d’une équipe projet ne sachant pas forcément développer (que nous appellerons “moldu” dans cet article). Ce point est donc une force puisque l’écriture des tests peut être réalisée par n’importe qui (au moins en partie). Néanmoins, au delà du Gherkin, il est également nécessaire d’écrire la “glue” entre les lignes de nos features et les actions à exécuter.

Postman

L’approche proposée par Postman est légèrement différente. Au delà d’un simple client REST doté d’une GUI, nous pouvons avec cet outil créer de véritables campagnes de tests comme le montre le screenshot suivant (1) :

Les différentes vérifications (code http de la réponse, contenu du body, …) sont définies en Javascript dans l’onglet “Test” (2). Il est également possible de créer des configurations par environnement (3) dont les paramètres sont réutilisables dans nos campagnes (4). Mais le véritable intérêt de cette démarche réside dans le fait de pouvoir externaliser ses campagnes en Json (et pourquoi pas les versionner avec le code source de l’application) afin de les exécuter automatiquement par un outil de CI via Newman (version en ligne de commande de Postman).
Cette méthode est moins complexe que la précédente : moins de code à écrire, syntaxe relativement simple, tests des campagnes possibles via l’interface… Cependant nous sommes dépendant d’un outil tiers pour éditer nos scénarios (les Json exportés n’étant pas très “human readable”).

Il ne s’agit ici que d’un échantillon des outils disponibles (nous n’avons pas parlé de Cypress ou de plateformes plus complexes comme Cerberus), mais Postman et Cucumber font sans doute partie des solutions les plus utilisées. Or vous remarquerez que dans chaque cas il y a des avantages, mais aussi quelques inconvénients.

… face à un véritable challenger

Créé par Peter Thomas au sein d’Intuit, Karate se présente sous la forme d’un framework OpenSource combinant automatisation des tests API, création de mocks et de scénarios, et tests de performances.

Le nombre de features proposées est juste allucinant, et parmi les plus importantes :

  • Création de tests et de scénarios pour évaluer la non régression d’API.
  • Création de mocks, très utile dans le cas où on souhaite, par exemple, valider notre développement, sans être branché sur un véritable environnement (base de données,  APIs tiers…)
  • Support de l’asynchronisme, ce qui signifie qu’il est possible d’intégrer de façon transparente la gestion des événements provenant de files de messages.
  • Evaluation des réponses dynamiques d’API GraphQL.
  • Support de REST, mais aussi de SOAP, Websocket, gRPC…
  • Intégration de Gatling, permettant d’évaluer la performance de votre application dans vos scénarios.
  • Exécution en parallèle des tests, pouvant vous faire gagner un temps précieux dans le cas de tests d’intégrations ou End to End.
  • Intégration avec JUnit 4 et 5, mais également utilisable en standalone.
  • Automatisation des tests UI (Karate ne se limite pas seulement aux tests APIs !).

Mais au-delà de ces fonctionnalités, ce qui semble le plus intéressant est sans doute l’approche offerte par le framework. L’intégralité des scénarios est écrit en Gherkin (les fans de Cucumber apprécieront 😉), et le plus beau dans tout cela : les actions utilisées dans le DSL sont déjà traduites, nul besoin de Java. On fait du Gherkin et que du Gherkin ! Et c’est là un des atouts majeurs de Karate sur ces concurrents.

  • Le Gherkin est compréhensible et peut facilement être pris en main par les moldus : nos amis les QA sont donc capables d’écrire les tests tout comme les développeurs.
  • Les scénarios peuvent facilement être embarqués avec le code source de l’application, et pourront être versionnés de la même manière.
  • Contrairement à Postman, plus besoin d’un outil tiers pour écrire les scénarios (votre IDE suffit).

Mais tout comme pour l’art martial, Karate s’apprend en pratiquant alors trêve de théorie et mettons en œuvre quelques Kata.

Hajime !

Pour démontrer la force de frappe de Karate, quoi de mieux qu’une mise en pratique. Pour cela nous allons mettre en oeuvre quelques tests de régressions sur le backend d’une application simpliste : la gestion d’une Todo List. L’API qui sera testée n’est donc qu’un simple CRUD écrit en Quarkus : après s’être authentifié, un utilisateur peut lister, créer, supprimer et rechercher des “todo lists” sauvegardées dans une base MongoDB.

Le code complet est disponible ici.

Nos premiers scénarios

La première étape pour utiliser Karate au sein de notre projet consiste à ajouter les dépendances Maven dont nous allons avoir besoin :

...
<dependency>
      <groupId>com.intuit.karate</groupId>
      <artifactId>karate-apache</artifactId>
      <version>0.9.5</version>
      <scope>test</scope>
</dependency>
<dependency>
      <groupId>com.intuit.karate</groupId>
      <artifactId>karate-junit5</artifactId>
      <version>0.9.5</version>
      <scope>test</scope>
</dependency>
...

Nous créons ensuite une classe FeatureRunner dans le répertoire test du projet et dont le rôle est référencer et exécuter les scénarios que nous allons écrire :

public class FeatureRunner {

    @Karate.Test
    Karate testNominalScenario() {
        return Karate.run("features/create-todo")
                .tags("@nominal")
                .relativeTo(getClass());
    }
}

Vous observerez deux choses importantes dans cette classe :

  • la présence de l’annotation @Karate.Test, nous permettant de préciser que la méthode testNominalScenario doit être exécutée comme test (les habitués de Junit ne seront normalement pas dépaysés)
  • l’utilisation de la méthode tags, prenant en argument la chaine de caractères @nominal. Celle-ci nous permet de filtrer les scénarios à exécuter pour tel ou tel fichier de features. Nous y reviendrons dans la suite de l’article.

Ecrivons à présent notre premier scénario. Tout comme avec Cucumber, les scénarios sont écrits en Gherkin et regroupés dans des fichiers portant l’extension .feature.

Le premier que nous allons créer correspond à un scénario nominal, puisqu’il consiste simplement à s’authentifier, à créer une todo, à la rechercher, puis à la supprimer (chaque étape se soldant par un succès).

Feature: Create new todo

  @nominal
  Scenario: Create todo should return 201 status
    # Authentication
    Given url 'http://localhost:8180/auth/realms/karate-quarkus-demo-realm/protocol/openid-connect/token'
    And request { username: 'todo-list-user', password: 'todo-list-user'}
    And form field username = 'todo-list-user'
    And form field password = 'todo-list-user'
    And form field client_id = 'karate-quarkus-demo'
    And form field client_secret = '19f745ce-5452-467f-bad8-ee14184240e5'
    And form field grant_type = 'password'
    When method POST
    Then status 200
    And def accessToken = response.access_token

    # Create a todo
    Given url 'http://localhost:8080/api/todos'
    And header Authorization = 'Bearer ' + accessToken
    And request {title: "course", description: "Aller faire les courses", priority : 1}
    When method POST
    Then status 201

    * def location = responseHeaders['Location'][0]

    # Search a todo
    Given url location
    And header Authorization = 'Bearer ' + accessToken
    And header Content-Type = 'application/json'
    When method GET
    Then status 200
    And match response contains {title: 'course', priority: 1}

    # Remove a todo
    Given url location
    And header Authorization = 'Bearer ' + accessToken
    And header Content-Type = 'application/json'
    When method DELETE
    Then status 204

Ici notre scénario est annoté avec @nominal, libellé qui est utilisé par la méthode tags que nous avons précédemment décrit et qui, pour rappel, permet de spécifier les groupes de scénarios à exécuter pour telle ou telle feature (cette valeur étant libre, nous aurions pu spécifier n’importe quel libellé pour taguer notre scénario).

Cette fonctionnalité est très pratique dans le cas où, par exemple, nous souhaiterions automatiser le lancement des tests via notre outil d’intégration continu préféré : nous pourrions alors très bien envisager de scheduler une exécution automatique de certains cas de tests lors des commits sur feature branches, ou jouer périodiquement l’intégralité des tests depuis la branche develop sur un environnement dédié.

Pour en revenir au scénario, ce dernier est écrit en Gherkin. Reprenons la première étape pour en décrire la syntaxe.

# Authentication
Given url 'http://localhost:8180/auth/realms/karate-quarkus-demo-realm/protocol/openid-connect/token'
And request { username: 'todo-list-user', password: 'todo-list-user'}
And form field username = 'todo-list-user'
And form field password = 'todo-list-user'
And form field client_id = 'karate-quarkus-demo'
And form field client_secret = '19f745ce-5452-467f-bad8-ee14184240e5'
And form field grant_type = 'password'
When method POST
Then status 200
And def accessToken = response.access_token
  • En premier lieu nous spécifions l’url qui sera appelée via le mot clé Given (ligne 2)
  • Les paramètres sont passés via une succession de And, permettant de préciser les valeurs de chaque champ (ligne 3 à 8).
  • L’appel via le verbe POST est réalisé en utilisant le mot clé When (ligne 9).
  • On contrôle qu’en retour nous obtenons le code Http 200 (ligne 10).
  • Nous terminons en stockant l’access_token, extrait de la réponse, dans une variable réutilisable dans les étapes suivantes (ligne 11).

Vous l’aurez compris, l’approche Given When Then est quasi identique à celle de Cucumber !

Ecrivons dans la foulée un second scénario, permettant de vérifier le bon fonctionnement de notre API lorsqu’on créé une todo avec des paramètres manquants (ici une todo sans “title” devra retourner une 400 Bad Request).

  Scenario: Create todo without title should return a 400 status
    # Authentication
    Given url 'http://localhost:8180/auth/realms/karate-quarkus-demo-realm/protocol/openid-connect/token'
    And request { username: 'todo-list-user', password: 'todo-list-user'}
    And form field username = 'todo-list-user'
    And form field password = 'todo-list-user'
    And form field client_id = 'karate-quarkus-demo'
    And form field client_secret = '19f745ce-5452-467f-bad8-ee14184240e5'
    And form field grant_type = 'password'
    When method POST
    Then status 200
    And def accessToken = response.access_token

    # Create a todo
    Given url 'http://localhost:8080/api/todos'
    And header Authorization = 'Bearer ' + accessToken
    And request {description: "Aller faire les courses", priority : 1}
    When method POST
    Then status 400

Optimiser nos scénarios et parfaire notre technique

Vous remarquerez que plusieurs choses peuvent être améliorées dans nos scénarios :

  • La phase d’authentification est toujours la même, quelque soit le scénario. Celle-ci pourrait être centralisée afin d’éviter les duplications.
  • De la même manière, les données utilisées pour créer les todos sont dupliquées dans chaque test.
  • Les URL et credentials sont en dur dans nos scénarios, les rendant inutilisables si nous changeons d’environnement (à moins d’en refactorer le code).

Factorisation du code

Tout d’abord voyons comment centraliser l’étape d’authentification et en faire une “fonction” réutilisable.

Commençons par créer une nouvelle feature dans fichier authentification.feature :

Feature: Reusable authentication feature

  Scenario:
    Given url authUrl
    And request { username: '#(username)', password: '#(password)', clientId: '#(clientId)', clientSecret: '#(clientSecret)'}
    And form field username = username
    And form field password = password
    And form field client_id = clientId
    And form field client_secret = clientSecret
    And form field grant_type = 'password'
    When method POST
    Then status 200
    And def accessToken = response.access_token

Le code est le même que précédemment, nous avons simplement variabilisé certains paramètres comme le username ou le password.

Ajoutons ensuite le bloc suivant au début de create-todo.feature

  Background:
    * def signIn = call read('authentication.feature') { username: 'todo-list-user', password: 'todo-list-user', clientId: 'karate-quarkus-demo', clientSecret: '19f745ce-5452-467f-bad8-ee14184240e5' }
    * def accessToken = signIn.accessToken

De cette façon nous “invoquons” la step d’authentification (comme nous pourrions le faire avec une méthode en Java) et nous stockons l’access_token obtenu dans une variable réutilisable dans l’ensemble de nos scénarios.

A ce stade, nous pouvons retirer les steps d’authentification de nos scénarios, cette phase étant désormais prise en charge par le bloc précédemment décrit.

Externalisation des données

A présent, voyons comment externaliser les données utilisées dans nos requêtes, au sein d’un fichier json réutilisable dans nos scénarios.

Commençons par créer un répertoire “data” dans lequel nous ajouterons ensuite le fichier contenant le json suivant :

{
  "title" : "course",
  "description" : "Aller faire les courses",
  "priority" : 1
}

Modifions le fichier create-todo.feature comme suit :

  Background:
    ...
    * def todoJson = read('data/todo.json')

Le bloc précédent permet de placer le contenu de todo.json dans la variable todoJson. Nous pourrons ensuite réutiliser cette variable dans nos scénarios :

 @nominal
  Scenario: Create todo should return 201 status
    # Create a todo
    Given url 'http://localhost:8080/api/todos'
    And header Authorization = 'Bearer ' + accessToken
    And request todoJson
    When method POST
    Then status 201
...

Gestion des configurations

Comme nous l’évoquions un peu plus haut, les urls et identifiants nécessaires au déroulement de nos scénarios (et utilisés notamment lors de l’authentification) sont définis en dur. Au delà de l’aspect sécurité, cela limite la souplesse de nos features : impossible d’exécuter une campagne de tests sur des environnements différents, si il n’est pas possible de switcher de configuration dans nos scénarios.

Encore une fois Karate a la solution.

Créons un fichier karate-config.js dans le dossier resources du répertoire test de notre projet. Ce dernier comportera une simple fonction :

function init() {
    var env = karate.env;
    karate.log('karate.env selected environment was:', env);
    if (!env) {
        env = 'local';
    }
    var config = {
        env: env,
        clientId: 'karate-quarkus-demo',
        clientSecret: '19f745ce-5452-467f-bad8-ee14184240e5',
        authUrl: 'http://localhost:8180/auth/realms/karate-quarkus-demo-realm/protocol/openid-connect/token',
        apiBaseUrl: 'http://localhost:8080/api'
    };
    if (env == 'dev') {
        config.clientId= 'karate-quarkus-demo',
        config.clientSecret= '29f745af-5452-487f-bad8-ee14354141a9',
        config.authUrl= 'http://192.168.1.17:8180/auth/realms/karate-quarkus-demo-realm/protocol/openid-connect/token',
        config.apiBaseUrl= 'http://192.168.1.17:8080/api'
    } else if (env == 'qa') {
        //...
    }
    karate.configure('connectTimeout', 5000);
    karate.configure('readTimeout', 5000);
    return config;
}

Nous créons une configuration par défaut (ligne 7 à 13) pouvant être surchargée en fonction d’une variable d’environnement (ligne 14 à 21).

Les paramètres définis dans ce fichier sont utilisables librement dans les features :

...    
* def resourceUrl = apiBaseUrl + '/todos'
...

@nominal
Scenario: Create todo should return 201 status
  # Create a todo
  Given url resourceUrl
  And header Authorization = 'Bearer ' + accessToken
  And request todoJson
  When method POST
  Then status 201

Et voilà ! Il ne reste plus qu’à lancer nos tests via la commande :

mvn clean test -DargLine="-Dkarate.env=local" -Dtest=FeatureRunner 

Comme indiqué dans la console, un rapport html contenant le détail de l’exécution est généré.

Des APIs plus rapides que Bruce Lee ?

Il ne pratiquait pas le karaté certes, mais c’est une légende !

Nous nous écartons un peu des tests de non régressions mais je ne pouvais pas conclure sur Karate sans vous parler rapidement de son intégration avec Gatling. Nous l’évoquions au début de cet article : il est possible de tester la performance de vos APIs avec Karate, et ce simplement en ajoutant une dépendance au projet :

<dependency>
    <groupId>com.intuit.karate</groupId>
    <artifactId>karate-gatling</artifactId>
    <scope>test</scope>
</dependency>  

A partir de là, vous pouvez réutiliser vos scénarios Karate en tant que tests de performances exécutés par Gatling. Le seul code à écrire sera le modèle de chargement des virtual users, tout le reste est réalisable à partir de Karate.
Vous disposerez même de quelques options pour distribuer vos tests de charges, en multipliant le nombre de noeuds à disposition par ou en passant par des conteneurs Docker par exemple.

Nous avons donc une solution permettant de tester la non régression de nos APIs, mais également la performance en écrivant (presque) que du Gherkin ! Alors pourquoi s’en priver ?

Victoire par Ippon ?

Karate est un outil complet qui, malgré l’absence d’interface, saura se faire une place auprès des moldus : pas de code complexe à écrire, pas de glue pour traduire les actions. Les scénarios s’écrivent en Gherkin uniquement, ce qui permet à n’importe qui de s’y mettre !

Nous venons de voir qu’une infime partie des capacités de Karate mais cette première approche aura permis de démontrer que le framework n’a rien à envier à ses adversaires. Je ne peux donc que vous conseiller de le mettre en pratique : vous verrez, l’essayer c’est l’adopter !

Liens utiles

Quelques liens utiles pour ceux d’entre vous qui souhaiteraient remonter sur le tatami et aller plus loin avec Karate.