Test API REST con Cucumber

1. Panoramica

Questo tutorial fornisce un'introduzione a Cucumber, uno strumento comunemente usato per i test di accettazione degli utenti e come usarlo nei test API REST.

Inoltre, per rendere l'articolo autonomo e indipendente da qualsiasi servizio REST esterno, utilizzeremo WireMock, una libreria di servizi web di stubbing e derisione. Se vuoi saperne di più su questa libreria, fai riferimento all'introduzione a WireMock.

2. Gherkin - il linguaggio del cetriolo

Cucumber è un framework di test che supporta lo sviluppo guidato dal comportamento (BDD), consentendo agli utenti di definire le operazioni dell'applicazione in testo normale. Funziona sulla base del Gherkin Domain Specific Language (DSL). Questa semplice ma potente sintassi di Gherkin consente a sviluppatori e tester di scrivere test complessi mantenendoli comprensibili anche agli utenti non tecnici.

2.1. Introduzione a Gherkin

Gherkin è un linguaggio orientato alla riga che utilizza terminazioni di riga, rientri e parole chiave per definire i documenti. Ogni riga non vuota di solito inizia con una parola chiave Gherkin, seguita da un testo arbitrario, che di solito è una descrizione della parola chiave.

L'intera struttura deve essere scritta in un file con l' estensione della caratteristica per essere riconosciuta da Cucumber.

Ecco un semplice esempio di documento 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

Nelle sezioni seguenti, descriveremo un paio degli elementi più importanti in una struttura Gherkin.

2.2. Caratteristica

Usiamo un file Gherkin per descrivere una funzionalità dell'applicazione che deve essere testata. Il file contiene la parola chiave Feature all'inizio, seguita dal nome della funzionalità sulla stessa riga e da una descrizione opzionale che può estendersi su più righe sottostanti.

Il parser di cetriolo salta tutto il testo, ad eccezione della parola chiave Feature , e lo include solo a scopo di documentazione.

2.3. Scenari e passaggi

Una struttura Gherkin può essere costituita da uno o più scenari, riconosciuti dalla parola chiave Scenario . Uno scenario è fondamentalmente un test che consente agli utenti di convalidare una capacità dell'applicazione. Dovrebbe descrivere un contesto iniziale, eventi che possono accadere e risultati attesi creati da quegli eventi.

Queste cose vengono eseguite utilizzando passaggi, identificati da una delle cinque parole chiave: Dato , Quando , Allora , E e Ma .

  • Dato : questo passaggio consiste nel mettere il sistema in uno stato ben definito prima che gli utenti inizino a interagire con l'applicazione. Una data clausola può essere considerata una precondizione per il caso d'uso.
  • Quando : un passaggio Quando viene utilizzato per descrivere un evento che si verifica nell'applicazione. Può trattarsi di un'azione intrapresa dagli utenti o di un evento attivato da un altro sistema.
  • Quindi : questo passaggio serve a specificare un risultato previsto del test. Il risultato dovrebbe essere correlato ai valori di business della caratteristica sotto test.
  • And and But : queste parole chiave possono essere utilizzate per sostituire le parole chiave dei passaggi precedenti quando sono presenti più passaggi dello stesso tipo.

Cucumber in realtà non distingue queste parole chiave, tuttavia sono ancora lì per rendere la funzionalità più leggibile e coerente con la struttura BDD.

3. Implementazione di Cucumber-JVM

Cucumber è stato originariamente scritto in Ruby ed è stato portato in Java con l'implementazione Cucumber-JVM, che è l'argomento di questa sezione.

3.1. Dipendenze di Maven

Per poter utilizzare Cucumber-JVM in un progetto Maven, è necessario includere nel POM la seguente dipendenza:

 io.cucumber cucumber-java 6.8.0 test 

Per facilitare il test di JUnit con Cucumber, dobbiamo avere un'altra dipendenza:

 io.cucumber cucumber-junit 6.8.0 

In alternativa, possiamo utilizzare un altro artefatto per sfruttare le espressioni lambda in Java 8, che non saranno trattate in questo tutorial.

3.2. Definizioni dei passaggi

Gli scenari di cetriolini sarebbero inutili se non fossero tradotti in azioni ed è qui che entrano in gioco le definizioni dei passaggi. Fondamentalmente, una definizione di passaggio è un metodo Java annotato con un modello allegato il cui compito è convertire i passaggi Gherkin in testo normale in codice eseguibile. Dopo aver analizzato un documento di funzionalità, Cucumber cercherà le definizioni dei passaggi che corrispondono ai passaggi Gherkin predefiniti da eseguire.

Per renderlo più chiaro, diamo un'occhiata al seguente passaggio:

Given I have registered a course in Baeldung

E una definizione del passaggio:

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

Quando Cucumber legge il passaggio specificato, cercherà le definizioni dei passaggi i cui schemi di annotazione corrispondono al testo del cetriolino.

4. Creazione ed esecuzione di test

4.1. Scrittura di un file di funzionalità

Iniziamo con la dichiarazione di scenari e passaggi in un file con il nome che termina con l' estensione .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

Salviamo ora questo file in una directory chiamata Feature , a condizione che la directory venga caricata nel classpath in fase di esecuzione, ad esempio src / main / resources .

4.2. Configurazione di JUnit per lavorare con Cucumber

In order for JUnit to be aware of Cucumber and read feature files when running, the Cucumber class must be declared as the Runner. We also need to tell JUnit the place to search for feature files and step definitions.

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

As you can see, the features element of CucumberOption locates the feature file created before. Another important element, called glue, provides paths to step definitions. However, if the test case and step definitions are in the same package as in this tutorial, that element may be dropped.

4.3. Writing Step Definitions

When Cucumber parses steps, it will search for methods annotated with Gherkin keywords to locate the matching step definitions.

A step definition’s expression can either be a Regular Expression or a Cucumber Expression. In this tutorial, we'll use Cucumber Expressions.

The following is a method that fully matches a Gherkin step. The method will be used to post data to a REST web service:

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

And here is a method matching a Gherkin step and takes an argument from the text, which will be used to get information from a REST web service:

@When("users want to get information on the {string} project") public void usersGetInformationOnAProject(String projectName) throws IOException { }

As you can see, the usersGetInformationOnAProject method takes a String argument, which is the project name. This argument is declared by {string} in the annotation and over here it corresponds to Cucumber in the step text.

Alternatively, we could use a regular expression:

@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:

  • parallela: possono essere classi, metodi o entrambi - nel nostro caso, le classi faranno eseguire ogni classe di test in un thread separato
  • threadCount: indica quanti thread devono essere allocati per questa esecuzione

Questo è tutto ciò che dobbiamo fare per eseguire le funzionalità di Cucumber in parallelo.

6. Conclusione

In questo tutorial, abbiamo trattato le basi di Cucumber e come questo framework utilizza il linguaggio specifico del dominio Gherkin per testare un'API REST.

Come al solito, tutti gli esempi di codice mostrati in questo tutorial sono disponibili su GitHub.