API-Testautomatisierung auf einfache Art und Weise?

Test-Framework Karate: APIs automatisiert testen

Alexander Frommelt

©Shutterstock / Blazej Lyjak

Sobald man APIs entwickelt, steht man vor dem Problem, wie man sie testet. Mit Karate gibt es ein Framework, das auf Basis von Cucumber eine einfache Möglichkeit bietet, den Test in die Softwareentwicklung zu integrieren.

In der heutigen Zeit, in der Microservices die Entwicklung bestimmen, startet fast jedes Projekt entweder mit der Definition oder Nutzung von APIs. Doch wie können sie sinnvoll getestet werden? Auf dem Markt haben sich dafür Postman oder SoapUI als Quasistandards durchgesetzt, die neben dem Test der API-Schnittstellen auch die Entwicklung und Definition beispielsweise durch Designwerkzeuge und Mocks mit mächtigen Oberflächen unterstützen. Auf der anderen Seite stellt sich immer wieder heraus, dass diese Tools nur schwer in einem Team einzusetzen sind, um gemeinsam an APIs und deren Tests zu arbeiten, insbesondere wenn man nicht die kommerzielle Enterprise-Variante der Tools einsetzen möchte.

Beide Tools erzeugen zur Ablage des API und der Testfallbeschreibungen große Datenfiles auf JSON- oder XML-Basis. Will man diese, wie allen anderen zur Anwendung gehörenden Code, in seiner Source-Verwaltung pflegen, so wird man merken, dass das nicht einfach ist. Plötzlich ist ein Testfall verschwunden oder lässt sich nicht mehr ausführen, da ein Kollege eine neue Version gespeichert hat. Aufgrund der komplexen Datenmodelle, in denen sowohl SoapUI als auch Postman ihre Daten ablegen, ist es schwierig, beim Merge den Überblick zu behalten und keine Inkonsistenzen zu erzeugen. Die Testfälle müssen jedoch während der Entwicklung kontinuierlich gepflegt und weiterentwickelt werden, da zum Beispiel neue Testfälle für neue Features dazukommen, Fehler in Testfällen bereinigt werden müssen oder das API an sich weiterentwickelt wird. Die landläufige Meinung, dass APIs etwas Statisches sind und damit keiner oder nur geringer Veränderung unterliegen, ist leider falsch. Selbst kleine Änderungen an der implementierenden Anwendung, beispielsweise an Validierungsregeln, können schon eine Änderung im Verhalten des API verursachen. Um das frühzeitig zu erkennen und gegebenenfalls Maßnahmen ergreifen zu können, ist es sinnvoll, die API sowohl für den Consumer als auch für den Producer kontinuierlich automatisiert zu testen.

Viel einfacher wäre es doch, eine simple Beschreibung der Testfälle in Form von Textfiles zu haben, quasi API-Testautomatisierung-as-Code. Diese Beschreibung wäre auch problemlos über die Source-Code-Verwaltung zu pflegen. Ein weiterer Vorteil davon wäre, dass die Testfälle immer dem aktuellen Source-Stand entsprechen und über Branching und Tagging jederzeit wieder konsistent zum Source-Stand reproduziert werden können.

Das Open Source Framework Karate der Firma Intuit bietet uns genau diese Möglichkeit. Der Anspruch von Karate ist es, API-Testautomatisierung, Mocks und Performance Testing in einem einzigen einheitlichen Framework zu kombinieren. Karate setzt dazu auf der BDD-Syntax von Cucumber/Gherkin auf, sodass auch Tester ohne Entwicklerhintergrund Tests schreiben können. Die einzelnen Tests können parallel ausgeführt werden, um die Performance zu testen, da Performance essentiell für APIs ist.

Im Unterschied zu Cucumber müssen keine Step Definitions oder zusätzlicher Glue Code selbst geschrieben werden. In Karate sind alle Step Definitions, die für den Zugriff auf HTTP, JSON und XML benötigt werden, bereits implementiert (Tabelle 1). Außerdem kann Karate auf einfache Art und Weise durch JavaScript-Funktionen erweitert werden. Das bedingt jedoch, dass die Syntax von Karate nicht, wie von Cucumber gewünscht, wie eine natürliche Sprache lesbar ist, sondern eine feste Form besitzt und damit eine kleine Programmiersprache beziehungsweise DSL für die Implementierung von Tests ist.

First Steps

Um Karate zu nutzen, müssen nur zwei zusätzliche Abhängigkeiten im Maven-pom.xml-File entsprechend Listing 1 ergänzt werden. Anschließend kann bereits ein erstes einfaches Testszenario geschrieben werden. Hierzu wird unter src/test/java der Ordner test-api und eine einfache Testklasse entsprechend Listing 2 angelegt. Das Beispiel setzt auf JUnit 4 auf. Ein Beispiel für JUnit 5 findet sich in der Dokumentation von Karate. Testfälle in Karate heißen Szenarien und sind wie in Listing 3 aufgebaut.

<dependency>
  <groupId>com.intuit.karate</groupId>
  <artifactId>karate-junit4</artifactId>
  <version>0.9.4</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>com.intuit.karate</groupId>
  <artifactId>karate-ui</artifactId>
  <version>0.9.4</version>
  <scope>test</scope>
</dependency>
package api-test;

import com.intuit.karate.junit4.Karate;
import org.junit.runner.RunWith;

@RunWith(Karate.class)
public class APITestRunner {

}
Feature <Beschreibung des Features>

Szenario: <Beschreibung des Szenarios>
Given url „http://localhost:8080/myServices/messages/sayHelloTo“
And request {name = “John Doe”}
When method post
Then status 200
And match response == {name=“John Doe“, message=“Guten Morgen John Doe“}

Das Feature wird im File helloWorld.feature abgelegt. Ein Feature kann mehrere Szenarien enthalten. Für größere Testautomatisierungsprojekte hat es sich bewährt, die Karate-Tests in einem eigenen Maven-Projekt zu pflegen. Mit der Runner-Klasse aus Listing 2 können die Features im gleichen Package und seinen Subpackages auch problemlos als JUnit-Tests in der IDE ausgeführt werden.

Führt man die Tests mit einer JUnit-Runner-Klasse aus, findet man anschließend im Verzeichnis target/surefire-reports einen HTML-Report. Diesen kann man sich im Browser ansehen. Er enthält sehr nützliche Informationen, die bei der Fehlersuche helfen, da die Raw Requests und Responses zusammen mit den einzelnen Schritten aus dem Szenario und den Fehlermeldungen aufgeführt werden.

Lesen Sie auch: Test-Framework Karate erreicht Milestone Release

Parallelisierung und Continuous Integration

Um Karate-Tests parallel auszuführen, benötigt man eine weitere Testklasse, die keine JUnit-Klasse, sondern spezifisch für Karate ist. Diese ist in Listing 4 beschrieben. Sehr häufig sind für Continuous Integration und Deployment verschiedene Umgebungen wie Entwicklung (DEV), User Acceptance Test (UAT), Integration und Produktion definiert. Vielleicht will man nicht alle Testfälle in allen Umgebungen laufen lassen. Oder es gibt Testfälle, die immer laufen (wie Regressionstestfälle) und solche, die nur unter bestimmten Umständen laufen sollen. Dafür gibt es in Karate die Möglichkeit, globale Properties für alle Karate-Tests zu definieren. Beim Start-up von Karate wird dazu das JavaScript-File karate-config.js aufgerufen. Es enthält eine Funktion, die als Ergebnis ein JSON-Objekt liefert. Die Properties aus dem JSON-Objekt stehen anschließend allen Testfällen zur Verfügung. Außerdem können zentrale Einstellungen beispielsweise für Timeouts oder Environment-Einstellungen für unterschiedliche Umgebungen gesetzt werden. In Listing 5 ist ein Beispiel zu sehen.

Damit ist die Basis geschaffen, um Karate-Tests in einer Continuous Integration und Deployment Pipeline zu nutzen. Nach der Anwendung von Build und Deployment auf einem Testsystem können die Karate-Tests automatisiert mit den entsprechenden Environment-Parametern ausgeführt und ausgewertet werden. Sind alle Tests erfolgreich, kann die Anwendung auf die nächste Stage deployt oder in Richtung Produktion freigegeben werden.

Komplexeres Szenario

Bisher wurde nur ein triviales Beispiel für einen Testfall betrachtet. Natürlich ist die Welt nur selten so einfach. Daher soll im Folgenden auch ein Blick auf die Implementierung komplexerer Szenarien geworfen werden.

Im ersten Schritt sollen Daten aus der Response des ersten Aufrufs an einen zweiten Aufruf weitergegeben werden. Karate bietet JSONPath zum Zugriff auf JSON an. Hierüber kann man sehr einfach auf Attribute im JSON-Format zugreifen.

def myParam = response.greetingMessage

myParam kann anschließend als Kriterium in einem Queryparameter benutzt werden.

And param name = myParam

Werden größere JSON- oder XML-Datenobjekte benötigt, können sie sinnvollerweise aus einer eigenen Datei gelesen und einer Variablen zugewiesen oder direkt als Request übergeben werden.

Given request = read (classpath:someJsonObject.json)

Analog kann auch die Response gegen ein JSON-Objekt aus der Datei validiert werden.

Then match response == read (classpath:someJsonObject.json)

Wer gerne Daten aus Tabellen benutzen möchte, um Arrays zu befüllen, kann auf das aus Cucumber bekannte Tabellen-Konstrukt zurückgreifen.

And table namens-liste
| name | breakfast |
| John | ja |
| Maria | nein |
| Manuel | {brot = „schwarzbrot“, belag=[„Marmelade“, „Butter“]} |

Die erste Zeile definiert die Spaltennamen, über die später auf die Spalten zugegriffen werden kann. In die Zellen können auch JSON-Blöcke gespeichert werden. Ein Zugriff auf eine Tabellenzeile sieht wie folgt aus:

match namens-liste[2] = {name = “Manuel”, {brot = „schwarzbrot“, belag=[„Marmelade“, „Butter“]}}

Test-driven Development

SoapUI und Postman legen eine API-zentrierte Vorgehensweise nahe, das heißt, die Entwicklung beginnt mit der Definition des API. In der agilen Softwareentwicklung hat sich jedoch mit Test-driven Development das Prinzip durchgesetzt, mit Tests zu starten. Das Karate Framework bietet auch hier die Möglichkeit, das Prinzip „Test first“ anzuwenden und bereits sehr früh mit dem Schreiben von Tests zu beginnen.

Bei der Erstellung von User Stories werden bereits Akzeptanztests definiert. Diese können bereits als Karate-Tests erstellt und initial auch von Nichtentwicklern geschrieben werden. Im Laufe der Entwicklung können diese Testfälle immer weiter verfeinert und vervollständigt werden. Hat man in die Feature- beziehungsweise Szenario-Definition einen Identifier wie eine Jira-Ticketnummer einer User Story oder eines Bugs aufgenommen, kann man im Fehlerfall sehr schnell die notwendigen Informationen finden, um zu beurteilen, ob der Testfall einen Fehler hat oder die Anwendung einen Bug enthält.

Da Karate auch die Laufzeit der einzelnen Schritte aufzeichnet, kann man auf dieser Basis sehr schnell sehen, wenn eine Funktion sich nicht innerhalb der erwarteten Antwortzeiten bewegt, und weitere Performance-Untersuchungen starten.

import com.intuit.karate.KarateOptions;
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import static org.junit.Assert.*;
import org.junit.Test;

@KarateOptions(tags = {"~@ignore"})
public class TestParallel {

  @Test
  public void testParallel() {
    Results results = Runner.parallel(getClass(), 5, "target/surefire-reports");
    assertTrue(results.getErrorMessages(), results.getFailCount() == 0);
  }

}
function fn() {   
  var env = karate.env; // get java system property 'karate.env'
  karate.log('karate.env system property was:', env);
  if (!env) {
    env = 'dev'; // a custom 'intelligent' default
  }
  var config = { // base config JSON
    appId: 'my.app.id',
    appSecret: 'my.secret',
    myServicesUrlBase: ' http://localhost/myServices/',
    anotherUrlBase: 'https://another-host.com/v1/'
  };
  if (env == 'dev') {
    // stage spezifische URLs
    config.myServicesUrlBase = 'http://localhost/myServices/';
  } else if (env == 'uat') {
    config.myServicesUrlBase = 'https://<your-test-domain>/myServices';
  }
  // Timeouts für den Connection Aufbau und Read
  karate.configure('connectTimeout', 500);
  karate.configure('readTimeout', 5000);
  return config;
}
Bereich Karate built-in Step Definitions
Variables def | text | table | yaml | csv | string | json | xml | xmlstring | bytes | copy
Actions assert | print | replace | get | set | remove | configure | call | callonce | eval | read() | karate API
HTTP url | path | request | method | status | soap action | retry until
Request param | header | cookie | form field | multipart file | multipart field | multipart entity | params | headers | cookies | form fields | multipart files | multipart fields
Response response | responseBytes | responseStatus | responseHeaders | responseCookies | responseTime | requestTimeStamp
Assert match == | match != | match contains | match contains only | match contains any | match !contains | match each | match header | Fuzzy Matching | Schema Validation | contains short-cuts

Tabelle 1: Übersicht Karate Steps

Fazit

Mit dem Framework Karate ist es möglich, API-Testautomatisierung in den Entwicklungsprozess mit Continuous Integration und Deployment aufzunehmen. Außerdem kann auch Test-driven Development bereits auf der API-Ebene eingesetzt werden. In den genannten Beispielen konnte natürlich nur ein kleiner Ausschnitt der Möglichkeiten, die Karate bietet, betrachtet werden. Wer sich ausführlicher damit auseinandersetzen will, findet auf den Webseiten von Karate weitere Informationen.

Das Framework hat noch nicht die Version 1.0 erreicht, jedoch sollte das niemanden daran hindern, es einzusetzen. Die auf Cucumber/Gherkin basierende DSL von Karate fühlt sich zwar nicht wie eine vollständige Programmiersprache an und unterstützt auch nicht den aus Cucumber bekannten natürlichen Sprachansatz für Testfallbeschreibungen, sie ist jedoch für die Anforderungen der Testfallbeschreibung absolut ausreichend und mächtig genug. Durch diese Art von Testfallbeschreibung ist es auch Nichtentwicklern möglich, zumindest einfache Testszenarien zu definieren. Wer sich näher mit Karate beschäftigen will, findet hier eine Anleitung.

Geschrieben von
Alexander Frommelt
Alexander Frommelt
Alexander Frommelt verantwortet als Competence Center Leiter den Bereich IT-Consulting Versicherungen bei der adesso AG. Er beschäftigt sich bereits seit mehr als fünfzehn Jahren mit Software- und Systemarchitekturen. Eines seiner aktuellen Interessensgebiete sind Sprachassistenten und ihre Einsatzmöglichkeiten.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: