REST-API-Test mit Gurke

1. Übersicht

Dieses Tutorial enthält eine Einführung in Cucumber, ein häufig verwendetes Tool zum Testen der Benutzerakzeptanz, und dessen Verwendung in REST-API-Tests.

Um den Artikel in sich geschlossen und unabhängig von externen REST-Diensten zu machen, verwenden wir außerdem WireMock, eine Stubbing- und Mocking-Webdienstbibliothek. Wenn Sie mehr über diese Bibliothek erfahren möchten, lesen Sie bitte die Einführung zu WireMock.

2. Gurke - die Sprache der Gurke

Cucumber ist ein Testframework, das Behavior Driven Development (BDD) unterstützt und es Benutzern ermöglicht, Anwendungsvorgänge im Klartext zu definieren. Es basiert auf der Gherkin Domain Specific Language (DSL). Diese einfache, aber leistungsstarke Syntax von Gherkin ermöglicht Entwicklern und Testern das Schreiben komplexer Tests, während sie auch für nicht technische Benutzer verständlich bleibt.

2.1. Einführung in Gurke

Gurke ist eine zeilenorientierte Sprache, die Zeilenenden, Einrückungen und Schlüsselwörter zum Definieren von Dokumenten verwendet. Jede nicht leere Zeile beginnt normalerweise mit einem Gherkin-Schlüsselwort, gefolgt von einem beliebigen Text, der normalerweise eine Beschreibung des Schlüsselworts darstellt.

Die gesamte Struktur muss in eine Datei mit der Feature- Erweiterung geschrieben werden, die von Cucumber erkannt werden soll.

Hier ist ein einfaches Beispiel für ein Gurkendokument:

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

In den folgenden Abschnitten werden einige der wichtigsten Elemente einer Gurkenstruktur beschrieben.

2.2. Merkmal

Wir verwenden eine Gurkendatei, um eine Anwendungsfunktion zu beschreiben, die getestet werden muss. Die Datei enthält ganz am Anfang das Feature- Schlüsselwort, gefolgt vom Feature-Namen in derselben Zeile und einer optionalen Beschreibung, die mehrere Zeilen darunter umfassen kann.

Der Gurkenparser überspringt den gesamten Text mit Ausnahme des Feature- Schlüsselworts und schließt ihn nur zu Dokumentationszwecken ein.

2.3. Szenarien und Schritte

Ein Gherkin Struktur kann aus einem oder mehreren Szenarien besteht, von der anerkannten Scenario Schlüsselwort. Ein Szenario ist im Grunde ein Test, mit dem Benutzer eine Funktion der Anwendung überprüfen können. Es sollte einen anfänglichen Kontext, mögliche Ereignisse und erwartete Ergebnisse dieser Ereignisse beschreiben.

Diese Dinge werden unter Verwendung von Schritten, die von einem der fünf Schlüsselwort: Gegeben , Wenn , dann , und , und Aber .

  • Gegeben : Dieser Schritt besteht darin, das System in einen genau definierten Zustand zu versetzen, bevor Benutzer mit der Anwendung interagieren. Eine gegebene Klausel kann als Voraussetzung für den Anwendungsfall angesehen werden.
  • Wann : Ein Wann- Schritt wird verwendet, um ein Ereignis zu beschreiben, das mit der Anwendung passiert. Dies kann eine von Benutzern ausgeführte Aktion oder ein von einem anderen System ausgelöstes Ereignis sein.
  • Dann : In diesem Schritt wird ein erwartetes Testergebnis angegeben. Das Ergebnis sollte sich auf die Geschäftswerte des zu testenden Features beziehen.
  • Und und Aber : Diese Schlüsselwörter können verwendet werden, um die obigen Schrittschlüsselwörter zu ersetzen, wenn mehrere Schritte desselben Typs vorhanden sind.

Cucumber unterscheidet diese Schlüsselwörter nicht wirklich, sie sind jedoch immer noch vorhanden, um die Funktion lesbarer und konsistenter mit der BDD-Struktur zu machen.

3. Implementierung von Cucumber-JVM

Cucumber wurde ursprünglich in Ruby geschrieben und mit der Cucumber-JVM-Implementierung, die Gegenstand dieses Abschnitts ist, nach Java portiert.

3.1. Maven-Abhängigkeiten

Um Cucumber-JVM in einem Maven-Projekt verwenden zu können, muss die folgende Abhängigkeit in das POM aufgenommen werden:

 io.cucumber cucumber-java 6.8.0 test 

Um das Testen von JUnit mit Cucumber zu vereinfachen, benötigen wir eine weitere Abhängigkeit:

 io.cucumber cucumber-junit 6.8.0 

Alternativ können wir ein anderes Artefakt verwenden, um Lambda-Ausdrücke in Java 8 zu nutzen, die in diesem Lernprogramm nicht behandelt werden.

3.2. Schrittdefinitionen

Gurkenszenarien wären nutzlos, wenn sie nicht in Aktionen übersetzt würden, und hier kommen Schrittdefinitionen ins Spiel. Grundsätzlich ist eine Schrittdefinition eine mit Anmerkungen versehene Java-Methode mit einem angehängten Muster, deren Aufgabe es ist, Gurkenschritte im Klartext in ausführbaren Code umzuwandeln. Nach dem Parsen eines Feature-Dokuments sucht Cucumber nach Schrittdefinitionen, die mit vordefinierten Gherkin-Schritten übereinstimmen, die ausgeführt werden sollen.

Schauen wir uns zur Verdeutlichung den folgenden Schritt an:

Given I have registered a course in Baeldung

Und eine Schrittdefinition:

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

Wenn Cucumber den angegebenen Schritt liest, sucht sie nach Schrittdefinitionen, deren Anmerkungsmuster mit dem Gherkin-Text übereinstimmen.

4. Erstellen und Ausführen von Tests

4.1. Schreiben einer Feature-Datei

Beginnen wir mit der Deklaration von Szenarien und Schritten in einer Datei, deren Name auf der Erweiterung .feature endet :

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

Wir speichern diese Datei jetzt in einem Verzeichnis mit dem Namen Feature , unter der Bedingung, dass das Verzeichnis zur Laufzeit in den Klassenpfad geladen wird, z. B. src / main / resources .

4.2. JUnit für die Arbeit mit Gurken konfigurieren

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:

  • parallel: kann Klassen, Methoden oder beides sein - in unserem Fall führen Klassen jede Testklasse in einem separaten Thread aus
  • threadCount: Gibt an, wie viele Threads für diese Ausführung zugewiesen werden sollen

Das ist alles, was wir tun müssen, um die Cucumber-Funktionen parallel auszuführen.

6. Fazit

In diesem Tutorial haben wir die Grundlagen von Cucumber und die Verwendung der domänenspezifischen Sprache Gherkin zum Testen einer REST-API behandelt.

Wie üblich sind alle in diesem Tutorial gezeigten Codebeispiele auf GitHub verfügbar.