Test de l'API REST avec le 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.

De plus, 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 simulation. Si vous voulez en savoir plus sur cette bibliothèque, veuillez vous référer à l'introduction à WireMock.

2. Gherkin - la langue du concombre

Cucumber est un cadre de test qui prend en charge le développement piloté par le comportement (BDD), permettant aux utilisateurs de définir les opérations d'application en texte brut. Il fonctionne sur la base du langage spécifique au domaine Gherkin (DSL). Cette syntaxe simple mais puissante de Gherkin permet aux développeurs et aux testeurs d'écrire des tests complexes tout en les gardant compréhensibles même pour les utilisateurs non techniques.

2.1. Introduction à Gherkin

Gherkin est un langage orienté ligne utilisant 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' extension de fonction pour être reconnue par Cucumber.

Voici un exemple de document Gherkin simple:

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

Dans les sections suivantes, nous décrirons quelques-uns des éléments les plus importants d'une structure Gherkin.

2.2. Fonctionnalité

Nous utilisons un fichier Gherkin pour décrire une fonctionnalité d'application qui doit être testée. 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.

L'analyseur de concombre ignore tout le texte, à l'exception du mot clé Feature , et l'inclut à 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 capacité de l'application. Il doit décrire un contexte initial, les événements qui peuvent 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: Donné , Quand , Alors , Et et Mais .

  • Étant donné : cette étape consiste à mettre le système dans un état bien défini avant que les utilisateurs ne commencent à interagir avec l'application. Une clause Given peut être considérée comme une condition préalable au cas d'utilisation.
  • Quand : Une étape Quand est utilisée pour décrire un événement qui arrive à l'application. Il peut s'agir d'une action entreprise par les utilisateurs ou d'un événement déclenché par un autre système.
  • Ensuite : 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.
  • Et et Mais : ces mots clés peuvent être utilisés pour remplacer les mots clés d'étape ci-dessus lorsqu'il existe plusieurs étapes du même type.

Cucumber 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 BDD.

3. Implémentation Cucumber-JVM

Cucumber a été initialement écrit en Ruby et a été porté en Java avec l'implémentation Cucumber-JVM, qui est le sujet de cette section.

3.1. Dépendances de Maven

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

 io.cucumber cucumber-java 6.8.0 test 

Pour faciliter les tests JUnit avec Cucumber, nous devons avoir une autre dépendance:

 io.cucumber cucumber-junit 6.8.0 

Alternativement, nous pouvons utiliser un autre artefact pour tirer parti des expressions lambda dans Java 8, qui ne seront pas couvertes dans ce tutoriel.

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 des étapes 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 Gherkin en texte brut en code exécutable. Après avoir analysé un document d'entités, Cucumber recherchera les définitions d'étape qui correspondent aux étapes prédéfinies de Gherkin à exécuter.

Afin de clarifier les choses, jetons un œil à l'étape suivante:

Given I have registered a course in Baeldung

Et une définition d'étape:

@Given("I have registered a course in Baeldung") 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 Gherkin.

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 classpath lors de l'exécution, par exemple src / main / resources .

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

Pour que JUnit connaisse Cucumber et lit les fichiers de fonctionnalités lors de son exécution, la classe Cucumber doit être déclarée en tant que Runner . Nous devons également indiquer à JUnit l'endroit où rechercher les fichiers de fonctionnalités et les définitions d'étape.

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

Comme vous pouvez le voir, l' élément features de CucumberOption 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 le scénario de test et les définitions d'étape sont 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 des méthodes annotées avec des mots-clés Gherkin pour localiser les définitions d'étape correspondantes.

L'expression d'une définition d'étape peut être une expression régulière ou une expression de concombre. Dans ce didacticiel, nous utiliserons les expressions de concombre.

Voici une méthode qui correspond parfaitement à une étape 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 correspondant à une étape 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 {string} project") public void usersGetInformationOnAProject(String projectName) throws IOException { }

Comme vous pouvez le voir, la méthode usersGetInformationOnAProject prend un argument String , qui est le nom du projet. Cet argument est déclaré par {string} dans l'annotation et ici il correspond à Cucumber dans le texte de l'étape.

Alternativement, nous pourrions utiliser une expression régulière:

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

Note, the ‘^' and ‘$' which indicate the start and end of the regex accordingly. Whereas ‘(.+)' corresponds to the String parameter.

We'll provide the working code for both of the above methods in the next section.

4.4. Creating and Running Tests

First, we will begin with a JSON structure to illustrate the data uploaded to the server by a POST request, and downloaded to the client using a GET. This structure is saved in the jsonString field, and shown below:

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

To demonstrate a REST API, we use a WireMock server:

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

In addition, we'll use Apache HttpClient API to represent the client used to connect to the server:

CloseableHttpClient httpClient = HttpClients.createDefault();

Now, let's move on to writing testing code within step definitions. We will do this for the usersUploadDataOnAProject method first.

The server should be running before the client connects to it:

wireMockServer.start();

Using the WireMock API to stub the REST service:

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

Now, send a POST request with the content taken from the jsonString field declared above to the server:

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

The following code asserts that the POST request has been successfully received and handled:

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

The server should stop after being used:

wireMockServer.stop();

The second method we will implement herein is usersGetInformationOnAProject(String projectName ). Similar to the first test, we need to start the server and then stub the REST service:

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

Submitting a GET request and receiving a response:

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

We will convert the httpResponse variable to a String using a helper method:

String responseString = convertResponseToString(httpResponse);

Here is the implementation of that conversion helper method:

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; }

The following verifies the whole process:

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

Finally, stop the server as described before.

5. Running Features in Parallel

Cucumber-JVM natively supports parallel test execution across multiple threads. We'll use JUnit together with Maven Failsafe plugin to execute the runners. Alternatively, we could use Maven Surefire.

JUnit runs the feature files in parallel rather than scenarios, which means all the scenarios in a feature file will be executed by the same thread.

Let's now add the plugin configuration:

 maven-failsafe-plugin ${maven-failsafe-plugin.version}   CucumberIntegrationTest.java  methods 2     integration-test verify    

Note that:

  • parallèle: peut être des classes, des méthodes ou les deux - dans notre cas, les classes feront exécuter chaque classe de test dans un thread séparé
  • threadCount: indique combien de threads doivent être alloués pour cette exécution

C'est tout ce que nous devons faire pour exécuter les fonctionnalités de Cucumber en parallèle.

6. Conclusion

Dans ce didacticiel, nous avons couvert les bases de Cucumber et comment ce framework utilise le langage spécifique au domaine Gherkin pour tester une API REST.

Comme d'habitude, tous les exemples de code présentés dans ce didacticiel sont disponibles à l'adresse over sur GitHub.