REST-API-Test mit Gurke

REST-API-Test mit Gurke

1. Überblick

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

Darüber hinaus verwenden wir WireMock, eine Stubbing- und Mocking-Webservice-Bibliothek, um den Artikel eigenständig und unabhängig von externen REST-Diensten zu machen. Wenn Sie mehr über diese Bibliothek erfahren möchten, lesen Sie bitteintroduction to WireMock.

2. Gurke - Die Sprache der Gurke

Cucumber ist ein Testframework, das Behaviour Driven Development (BDD) unterstützt und es Benutzern ermöglicht, Anwendungsvorgänge im Klartext zu definieren. Es funktioniert basierend aufGherkin Domain Specific Language (DSL). Diese einfache, aber leistungsstarke Syntax von Gherkin ermöglicht es Entwicklern und Testern, komplexe Tests zu schreiben und diese auch für nicht technische Benutzer verständlich zu halten.

2.1. Einführung in Gurke

Gurke ist eine zeilenorientierte Sprache, die zur Definition von Dokumenten Zeilenenden, Einrückungen und Schlüsselwörter verwendet. Jede nicht leere Zeile beginnt normalerweise mit einem Gherkin-Schlüsselwort, gefolgt von einem beliebigen Text, bei dem es sich normalerweise um eine Beschreibung des Schlüsselworts handelt.

Die gesamte Struktur muss in eine Datei mit der Erweiterungfeaturegeschrieben werden, um von Cucumber erkannt zu werden.

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 Unterabschnitten werden einige der wichtigsten Elemente einer Gurkenstruktur beschrieben.

2.2. Feature

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

Der gesamte Text mit Ausnahme des SchlüsselwortsFeaturewird vom Cucumber-Parser übersprungen und nur zu Dokumentationszwecken eingefügt.

2.3. Szenarien und Schritte

Eine Gurkenstruktur kann aus einem oder mehreren Szenarien bestehen, die durch das SchlüsselwortScenarioerkannt werden. Ein Szenario ist im Grunde ein Test, mit dem Benutzer eine Funktion der Anwendung überprüfen können. Es sollte einen anfänglichen Kontext beschreiben, Ereignisse, die auftreten können, und erwartete Ergebnisse, die durch diese Ereignisse hervorgerufen werden.

Diese Dinge werden in Schritten ausgeführt, die durch eines der fünf Schlüsselwörter gekennzeichnet sind:Given,When,Then,And undBut.

  • Given: In diesem Schritt wird das System in einen genau definierten Zustand versetzt, bevor Benutzer mit der Anwendung interagieren. EineGiven-Klausel kann als Voraussetzung für den Anwendungsfall angesehen werden.

  • When: EinWhen-Schritt wird verwendet, um ein Ereignis zu beschreiben, das mit der Anwendung passiert. Dies kann eine Aktion sein, die von Benutzern ausgeführt wird, oder ein Ereignis, das von einem anderen System ausgelöst wird.

  • Then: In diesem Schritt wird ein erwartetes Testergebnis angegeben. Das Ergebnis sollte sich auf die Geschäftswerte des zu testenden Features beziehen.

  • And undBut: 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 mit der BDD-Struktur konsistenter zu machen.

3. Cucumber-JVM-Implementierung

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 im POM enthalten sein:


    info.cukes
    cucumber-java
    1.2.4
    test

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


    info.cukes
    cucumber-junit
    1.2.4

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, Gherkin-Schritte in Klartext in ausführbaren Code umzuwandeln. Nach dem Parsen eines Feature-Dokuments sucht Cucumber nach Schrittdefinitionen, die den auszuführenden vordefinierten Gherkin-Schritten entsprechen.

Schauen wir uns zur Verdeutlichung den folgenden Schritt an:

Given I have registered a course in example

Und eine Schrittdefinition:

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

Wenn Cucumber den angegebenen Schritt liest, sucht es nach Schrittdefinitionen, deren Beschriftungsmuster mit dem Gurkentext übereinstimmen. In unserer Abbildung wird festgestellt, dass die MethodetestMethodübereinstimmt, und der Code wird dann ausgeführt, sodass die ZeichenfolgeLet me in!auf der Konsole ausgedruckt wird.

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.featureendet:

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 NamenFeature, unter der Bedingung, dass das Verzeichnis zur Laufzeit in den Klassenpfad geladen wird, z. src/main/resources.

4.2. JUnit für die Arbeit mit Gurken konfigurieren

Damit JUnit Cucumber kennt und Feature-Dateien beim Ausführen liest, muss die KlasseCucumberalsRunnerdeklariert werden. Wir müssen JUnit auch mitteilen, wo nach Feature-Dateien und Schrittdefinitionen gesucht werden soll.

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

}

Wie Sie sehen können, findet dasfeatures-Element vonCucumberOption die zuvor erstellte Feature-Datei. Ein weiteres wichtiges Element,glue genannt, bietet Pfade zu Schrittdefinitionen. Befinden sich die Testfall- und Schrittdefinitionen jedoch im selben Paket wie in diesem Lernprogramm, wird dieses Element möglicherweise gelöscht.

4.3. Schrittdefinitionen schreiben

Wenn Cucumber Schritte analysiert, sucht es nach Methoden, die mit Gherkin-Schlüsselwörtern versehen sind, um die entsprechenden Schrittdefinitionen zu finden. In diesem Lernprogramm werden diese Schrittdefinitionen in einer Klasse innerhalb desselben Pakets mitCucumberTest definiert.

Das Folgende ist eine Methode, die einem Gherkin-Schritt vollständig entspricht. Die Methode wird verwendet, um Daten an einen REST-Webdienst zu senden:

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

}

Und hier ist eine Methode, die einem Gherkin-Schritt entspricht und ein Argument aus dem Text entnimmt, mit dem Informationen von einem REST-Webdienst abgerufen werden:

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

}

Wie Sie sehen können, verwendet die MethodeusersGetInformationOnAProjectdas ArgumentString, bei dem es sich um den Projektnamen handelt. Dieses Argument wird in der Annotation durch(.+) deklariert und entspricht hierCucumber im Schritttext.

Der Arbeitscode für beide oben genannten Methoden wird im nächsten Abschnitt bereitgestellt.

4.4. Erstellen und Ausführen von Tests

Zunächst werden wir mit einer JSON-Struktur beginnen, um die Daten zu veranschaulichen, die durch eine POST-Anforderung auf den Server hochgeladen und mit einem GET auf den Client heruntergeladen wurden. Diese Struktur wird im FeldjsonString gespeichert und unten angezeigt:

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

    "website": "cucumber.io"
}

Zur Demonstration einer REST-API kommt ein WireMock-Server ins Spiel:

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

Darüber hinaus wird in diesem Lernprogramm die Apache HttpClient-API verwendet, um den Client darzustellen, der zum Herstellen einer Verbindung zum Server verwendet wird:

CloseableHttpClient httpClient = HttpClients.createDefault();

Fahren wir nun mit dem Schreiben von Testcode in Schrittdefinitionen fort. Wir werden dies zuerst für dieusersUploadDataOnAProject-Methode tun.

Der Server sollte ausgeführt werden, bevor der Client eine Verbindung herstellt:

wireMockServer.start();

Verwenden der WireMock-API zum Stub des REST-Service:

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

Senden Sie nun eine POST-Anfrage mit dem Inhalt aus dem oben deklarierten FeldjsonStringan den Server:

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

Mit dem folgenden Code wird bestätigt, dass die POST-Anforderung erfolgreich empfangen und verarbeitet wurde:

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

Der Server sollte nach der Verwendung anhalten:

wireMockServer.stop();

Die zweite Methode, die wir hier implementieren werden, istusersGetInformationOnAProject(String projectName). Ähnlich wie beim ersten Test müssen wir den Server starten und dann den REST-Service beenden:

wireMockServer.start();

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

Senden einer GET-Anfrage und Empfangen einer Antwort:

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

Wir werden die VariablehttpResponse mit einer Hilfsmethode inString konvertieren:

String responseString = convertResponseToString(httpResponse);

Hier ist die Implementierung dieser Konvertierungshilfemethode:

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

Folgendes überprüft den gesamten Prozess:

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

Beenden Sie abschließend den Server wie zuvor beschrieben.

5. Funktionen parallel ausführen

Manchmal müssen wir die Funktionen parallel ausführen, um den Testprozess zu beschleunigen.

We can use the cucumber-jvm-parallel-plugin to create a separate runner for each feature/scenario. Dann konfigurieren wirmaven-failsafe-plugin so, dass die resultierenden Läufer parallel ausgeführt werden.

Zuerst müssen wircucumber-jvm-parallel-plugin zu unserenpom.xmlhinzufügen:


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

Wir können diecucumber-jvm-parallel-plugin leicht anpassen, da sie mehrere Parameter haben. Hier sind die, die wir benutzt haben:

  • glue.package: (obligatorisch) unser Integrationstestpaket

  • featuresDirectory: Der Pfad zum Verzeichnis enthält unsere Feature-Dateien

  • parallelScheme: kann entweder SCENARIO oder FEATURE sein, wobei SCENARIO einen Läufer pro Szenario und FEATURE einen Läufer pro Feature generiert

Jetzt werden wir auchconfigure the maven-failsafe-plugin to execute resulting runners in parallel:


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

Beachten Sie, dass:

  • parallel: kannclasses, methods oder beides sein - in unserem Fall lässtclasses jede Testklasse in einem separaten Thread laufen

  • threadCount: gibt an, wie viele Threads für diese Ausführung zugewiesen werden sollen

Dann können wir zum Ausführen der Tests den folgenden Befehl verwenden:

mvn verify

Wir werden feststellen, dass jedes Szenario in einem separaten Thread ausgeführt wird.

6. Fazit

In diesem Lernprogramm wurden die Grundlagen von Cucumber und die Verwendung der domänenspezifischen Sprache von Gherkin zum Testen einer REST-API erläutert.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie ina GitHub project.