Test de l’API REST avec concombre

Test API REST avec concombre

1. Vue d'ensemble

Ce didacticiel donne une introduction àCucumber, un outil couramment utilisé pour les tests d'acceptation des utilisateurs, et comment l'utiliser dans les tests d'API REST.

En outre, pour rendre l'article autonome et indépendant de tout service REST externe, nous utiliserons WireMock, une bibliothèque de services Web de stubbing et de moqueur. Si vous voulez en savoir plus sur cette bibliothèque, veuillez vous référer auxintroduction to WireMock.

2. Gherkin - La langue du concombre

Cucumber est une structure de test qui prend en charge le développement basé sur le comportement (BDD), permettant aux utilisateurs de définir les opérations de l'application en texte brut. Il fonctionne sur la base duGherkin Domain Specific Language (DSL). Cette syntaxe simple mais puissante de Gherkin permet aux développeurs et aux testeurs d’écrire des tests complexes tout en les rendant compréhensibles, même pour les utilisateurs non spécialistes.

2.1. Introduction à Gherkin

Gherkin est un langage orienté ligne qui utilise des fins de ligne, des indentations et des mots-clés pour définir des documents. Chaque ligne non vide commence généralement par un mot clé Gherkin, suivi d'un texte arbitraire, qui est généralement une description du mot clé.

La structure entière doit être écrite dans un fichier avec l'extensionfeature pour être reconnue par Cucumber.

Voici un exemple simple de document Gherkin:

Feature: A short description of the desired functionality

  Scenario: A business situation
    Given a precondition
    And another precondition
    When an event happens
    And another event happens too
    Then a testable outcome is achieved
    And something else is also completed

Les sous-sections ci-dessous décrivent quelques-uns des éléments les plus importants d’une structure de Gherkin.

2.2. Fonctionnalité

Un fichier Gherkin est utilisé pour décrire une fonctionnalité de l'application à tester. Le fichier contient le mot-cléFeature au tout début, suivi du nom de la fonction sur la même ligne et d'une description facultative qui peut s'étendre sur plusieurs lignes en dessous.

Tout le texte, à l'exception du mot-cléFeature, est ignoré par l'analyseur Cucumber et inclus à des fins de documentation uniquement.

2.3. Scénarios et étapes

Une structure Gherkin peut être constituée d'un ou plusieurs scénarios, reconnus par le mot cléScenario. Un scénario est essentiellement un test permettant aux utilisateurs de valider une fonctionnalité de l'application. Il devrait décrire un contexte initial, les événements pouvant survenir et les résultats attendus créés par ces événements.

Ces opérations sont effectuées à l'aide d'étapes identifiées par l'un des cinq mots-clés:Given,When,Then,And etBut.

  • Given: cette étape consiste à mettre le système dans un état bien défini avant que les utilisateurs ne commencent à interagir avec l'application. Une clauseGiven peut être considérée comme une condition préalable pour le cas d'utilisation.

  • When: une étapeWhen est utilisée pour décrire un événement qui arrive à l'application. Cela peut être une action entreprise par les utilisateurs ou un événement déclenché par un autre système.

  • Then: cette étape consiste à spécifier un résultat attendu du test. Le résultat doit être lié aux valeurs commerciales de la fonctionnalité testée.

  • And etBut: ces mots-clés peuvent être utilisés pour remplacer les mots-clés d'étape ci-dessus lorsqu'il y a plusieurs étapes du même type.

Le concombre ne distingue pas réellement ces mots-clés, mais ils sont toujours là pour rendre la fonctionnalité plus lisible et cohérente avec la structure de BDD.

3. Implémentation Cucumber-JVM

Cucumber a été à l'origine écrit en Ruby et a été porté en Java avec l'implémentation Cucumber-JVM, qui fait l'objet de cette section.

3.1. Dépendances Maven

Pour pouvoir utiliser Cucumber-JVM dans un projet Maven, la dépendance suivante doit être incluse dans le POM:


    info.cukes
    cucumber-java
    1.2.4
    test

Pour faciliter les tests de JUnit avec Cucumber, nous avons besoin d’une dépendance supplémentaire:


    info.cukes
    cucumber-junit
    1.2.4

Alternativement, nous pouvons utiliser un autre artefact pour tirer parti des expressions lambda dans Java 8, qui ne seront pas abordées dans ce didacticiel.

3.2. Définitions des étapes

Les scénarios Gherkin seraient inutiles s’ils n’étaient pas traduits en actions et c’est là que les définitions d’étape entrent en jeu. Fondamentalement, une définition d’étape est une méthode Java annotée avec un modèle attaché dont le travail consiste à convertir les étapes de Gherkin en texte brut en code exécutable. Après l'analyse d'un document d'entité, Cucumber recherchera les définitions d'étape correspondant aux étapes Gherkin prédéfinies à exécuter.

Afin de clarifier les choses, examinons l'étape suivante:

Given I have registered a course in example

Et une définition de l'étape:

@Given("I have registered a course in example")
public void verifyAccount() {
    // method implementation
}

Lorsque Cucumber lit l'étape donnée, il recherche les définitions d'étape dont les motifs d'annotation correspondent au texte de Gherkin. Dans notre illustration, la méthodetestMethod s'avère être une correspondance et son code est ensuite exécuté, ce qui entraîne l'impression de la chaîneLet me in! sur la console.

4. Création et exécution de tests

4.1. Écriture d'un fichier d'entités

Commençons par déclarer des scénarios et des étapes dans un fichier dont le nom se termine par l'extension.feature:

Feature: Testing a REST API
  Users should be able to submit GET and POST requests to a web service,
  represented by WireMock

  Scenario: Data Upload to a web service
    When users upload data on a project
    Then the server should handle it and return a success status

  Scenario: Data retrieval from a web service
    When users want to get information on the Cucumber project
    Then the requested data is returned

Nous sauvegardons maintenant ce fichier dans un répertoire nomméFeature, à la condition que le répertoire soit chargé dans le chemin de classe lors de l'exécution, par exemple src/main/resources.

4.2. Configurer JUnit pour qu'il fonctionne avec le concombre

Pour que JUnit soit conscient de Cucumber et lise les fichiers de fonctionnalités lors de l'exécution, la classeCucumber doit être déclarée en tant queRunner. Nous devons également indiquer à JUnit l'endroit où il convient de rechercher des fichiers de caractéristiques et des définitions d'étape.

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberTest {

}

Comme vous pouvez le voir, l'élémentfeatures deCucumberOption localise le fichier d'entités créé auparavant. Un autre élément important, appeléglue, fournit des chemins vers les définitions d'étape. Toutefois, si les définitions de scénario de test et d'étape se trouvent dans le même package que dans ce didacticiel, cet élément peut être supprimé.

4.3. Définition des étapes de rédaction

Lorsque Cucumber analyse les étapes, il recherche les méthodes annotées avec des mots clés Gherkin pour localiser les définitions d'étape correspondantes. Dans ce didacticiel, ces définitions d'étape sont définies dans une classe du même package avecCucumberTest.

Voici une méthode qui correspond parfaitement à une étape de Gherkin. La méthode sera utilisée pour publier des données sur un service Web REST:

@When("^users upload data on a project$")
public void usersUploadDataOnAProject() throws IOException {

}

Et voici une méthode qui correspond à une étape de Gherkin et prend un argument du texte, qui sera utilisé pour obtenir des informations d’un service Web REST:

@When("^users want to get information on the (.+) project$")
public void usersGetInformationOnAProject(String projectName) throws IOException {

}

Comme vous pouvez le voir, la méthodeusersGetInformationOnAProject prend un argumentString, qui est le nom du projet. Cet argument est déclaré par(.+) dans l'annotation et ici il correspond àCucumber dans le texte de l'étape.

Le code de travail pour les deux méthodes ci-dessus sera fourni dans la section suivante.

4.4. Création et exécution de tests

Tout d'abord, nous commencerons par une structure JSON pour illustrer les données téléchargées sur le serveur par une requête POST, puis téléchargées sur le client à l'aide d'un GET. Cette structure est enregistrée dans le champjsonString et illustrée ci-dessous:

{
    "testing-framework": "cucumber",
    "supported-language":
    [
        "Ruby",
        "Java",
        "Javascript",
        "PHP",
        "Python",
        "C++"
    ],

    "website": "cucumber.io"
}

Pour démontrer une API REST, un serveur WireMock entre en jeu:

WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());

De plus, ce tutoriel utilisera l'API Apache HttpClient pour représenter le client utilisé pour se connecter au serveur:

CloseableHttpClient httpClient = HttpClients.createDefault();

Passons maintenant à l'écriture du code de test dans les définitions d'étape. Nous allons d'abord faire cela pour la méthodeusersUploadDataOnAProject.

Le serveur doit être en cours d'exécution avant que le client ne s'y connecte:

wireMockServer.start();

Utilisation de l'API WireMock pour remplacer le service REST:

configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json"))
  .withRequestBody(containing("testing-framework"))
  .willReturn(aResponse().withStatus(200)));

Maintenant, envoyez une requête POST avec le contenu extrait du champjsonString déclaré ci-dessus au serveur:

HttpPost request = new HttpPost("http://localhost:" + wireMockServer.port() + "/create");
StringEntity entity = new StringEntity(jsonString);
request.addHeader("content-type", "application/json");
request.setEntity(entity);
HttpResponse response = httpClient.execute(request);

Le code suivant affirme que la demande POST a été reçue et traitée avec succès:

assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json")));

Le serveur devrait s'arrêter après avoir été utilisé:

wireMockServer.stop();

La deuxième méthode que nous allons implémenter ici estusersGetInformationOnAProject(String projectName). Comme pour le premier test, nous devons démarrer le serveur, puis stub le service REST:

wireMockServer.start();

configureFor("localhost", wireMockServer.port());
stubFor(get(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json"))
  .willReturn(aResponse().withBody(jsonString)));

Envoi d'une demande GET et réception d'une réponse:

HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/projects/" + projectName.toLowerCase());
request.addHeader("accept", "application/json");
HttpResponse httpResponse = httpClient.execute(request);

Nous allons convertir la variablehttpResponse enString en utilisant une méthode d'assistance:

String responseString = convertResponseToString(httpResponse);

Voici l'implémentation de cette méthode d'assistance à la conversion:

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

Ce qui suit vérifie l'ensemble du processus:

assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json")));

Enfin, arrêtez le serveur comme décrit précédemment.

5. Fonctionnalités d'exécution en parallèle

Parfois, nous devons exécuter les fonctionnalités en parallèle pour accélérer le processus de test.

We can use the cucumber-jvm-parallel-plugin to create a separate runner for each feature/scenario. Ensuite, nous allons configurer lesmaven-failsafe-plugin pour exécuter les coureurs résultants en parallèle.

Tout d'abord, nous devons ajouter lecucumber-jvm-parallel-plugin à nospom.xml:


  com.github.temyers
  cucumber-jvm-parallel-plugin
  5.0.0
  
    
      generateRunners
      generate-test-sources
      
        generateRunners
      
      
        
          com.example.rest.cucumber
        
        src/test/resources/Feature/
        SCENARIO
      
    
  

Nous pouvons facilement personnaliser lescucumber-jvm-parallel-plugin car il a plusieurs paramètres. Voici ceux que nous avons utilisés:

  • glue.package: (obligatoire) notre package de test d'intégration

  • featuresDirectory: le chemin d'accès au répertoire contient nos fichiers de fonctionnalités

  • parallelScheme: peut être SCENARIO ou FEATURE où SCENARIO génère un coureur par scénario et FEATURE génère un coureur par caractéristique

Maintenant, nous allons égalementconfigure the maven-failsafe-plugin to execute resulting runners in parallel:


    maven-failsafe-plugin
    2.19.1
    
        classes
        2
    
    
        
            
                integration-test
                verify
            
        
    

Notez que:

  • parallel: peut êtreclasses, methods ou les deux - dans notre cas,classes fera exécuter chaque classe de test dans un thread séparé

  • threadCount: indique le nombre de threads à allouer pour cette exécution

Ensuite, pour exécuter les tests, nous pouvons utiliser la commande:

mvn verify

Nous remarquerons que chaque scénario s'exécute dans un thread distinct.

6. Conclusion

Ce tutoriel couvre les bases du concombre et explique comment ce cadre utilise le langage spécifique au domaine Gherkin pour tester une API REST.

L'implémentation de tous ces exemples et extraits de code se trouve dansa GitHub project.