Endlich gute API-Tests
Der Traum von den unendlichen Weiten der Daten ist schnell ausgeträumt, wenn wir unsere APIs nicht im Griff haben. Dabei ist es egal, ob wir uns APIs zwischen unseren Services, zwischen Frontend und Backend oder zu Fremdsystemen anschauen – Testautomatisierung ist ein wesentlicher Bestandteil und Enabler dafür, dass wir unsere APIs überhaupt designen, entwickeln und bei Bedarf auch wieder anpassen können.
Das Problem hat sich in den letzten Jahren immer weiter verschärft: Wo früher wenige große monolithische Systeme interagiert haben, haben wir es heute oft mit einer Vielzahl von Microservices und deren Kommunikation zu tun. Statt relativ isolierter Tests mit einer klaren Verantwortung innerhalb des Systems haben wir sehr viel mehr Schnittstellen zwischen einzelnen Komponenten, die sauber abgesichert werden müssen.
Ein üblicher Ansatz, den wir in der Praxis immer wieder sehen, sind spezifische und vor allem testdatenabhängige Testfälle: Wir rufen die APIs auf und prüfen, ob genau das zurückkommt, was wir erwarten.
Doch dieser Ansatz als alleiniger Baustein einer API-Teststrategie hat gravierende Nachteile:
- Die Tests sind anfällig für sich verändernde Daten. In der Realität kann heute ein Ergebnis korrekt sein, welches gestern noch falsch war. Einfache Beispiele hierfür sind Counter oder Datumsangaben. Dadurch entstehen False Positives innerhalb der Tests. Im schlimmsten Fall führt das dazu, dass echte Fehler übersehen werden und im Grundrauschen der dadurch entstehenden erwarteten Fehlerraten untergehen.
- Bei schreibenden Zugriffen über APIs sind schon einfache Testaufrufe herausfordernd: Wir müssen viel Aufwand betreiben, um den Zustand unseres Gesamtsystems nicht zu verfälschen oder verzichten stattdessen darauf, diese schreibenden Zugriffe zu testen. Verschärft wird dies bei Interaktionen mit Fremdsystem-APIs, wo wir dadurch schon in unserem Test-Setup signifikante Abhängigkeiten und gegebenenfalls zusätzliche Limitierungen haben.
- Solche Tests decken vor allem den Aspekt "funktionale Korrektheit" ab. Andere Aspekte von API-Qualität werden nicht oder kaum abgedeckt.
Qualitätsaspekte von APIs – Was wollen wir überhaupt testen?
Wenn wir über API-Tests sprechen, sollten wir uns unbedingt die Frage stellen, welche Aspekte von APIs und ihrer Qualität relevant sind.
Hierbei lohnt ein Blick auf das arc42 Quality Model. Dieses benennt acht Top-Level-Eigenschaften von Software-Systemen: reliable, flexible, efficient, usable, safe, secure, suitable und operable. Diesen werden dann verschiedene Qualitätsaspekte zugeordnet [1].
Dabei haben wir bei klassischen API-Tests insbesondere den Qualitätsaspekt "Funktionale Korrektheit" in verschiedenen Ausprägungen oft selbstverständlich im Kopf: Wir rufen die APIs auf und testen dann, ob fachlich das erwartete Ergebnis geliefert wird. Hierfür testen wir einerseits isoliert einzelne Bestandteile unseres Gesamtsystems, indem wir Interaktionen mit der Außenwelt mocken, daneben sichern wir über Ende-zu-Ende-Tests, oft in Verbindung mit komplexem Testdatenmanagement, das fachliche Verhalten unseres Gesamtsystems ab. Gerade in Bezug auf APIs haben aber daneben auch die anderen Aspekte entscheidende Bedeutung.
Wir gehen im Folgenden auf Verifikation von API-Spezifikationen, Contract Tests und trace-basierte Tests genauer ein. Insbesondere betrachten wir, wie diese auf folgende Qualitätsaspekte besonders einzahlen:
- Understandability und Usability: Durch gute und konsistente Spezifikationen werden APIs einfach und sinnvoll nutzbar.
- Compatibility: Durch Contract Tests sind Anpassungen der APIs mit hoher Konfidenz möglich.
- Reliability und Performance Efficiency: Über trace-basierte Tests können wir auch diese Aspekte automatisiert verifizieren.
Gute APIs brauchen gute Spezifikationen – und deren Qualität können wir automatisiert testen
Existierende API-Spezifikationen sind häufig relativ vage oder ungenau. Es passiert in der Praxis leicht, dass etwas in der Spezifikation fehlt oder gar nicht allen Beteiligten klar und für diese offensichtlich ist, was Teil der Spezifikation sein muss. Ein Beispiel sind Rate Limits: Diese werden oft vom Anbieter der Schnittstelle vorgegeben bzw. zwischen den Schnittstellenpartnern vereinbart. Sind diese nicht Teil der Spezifikation, entsteht die Klarheit darüber häufig erst spät in der Umsetzung. Dazu kommt, dass Spezifikationen selbst innerhalb von Organisationen nicht einheitlich erstellt sind. Gerade in größeren Kontexten ist aber genau diese Vereinheitlichung relevant für eine gute Wartbarkeit und Nutzbarkeit der APIs, vor allem aber wichtiger als persönliche Vorlieben oder Geschmack.
Hierbei ist häufig der Ansatz, diese Vereinheitlichung über API-Guidelines und Prozesse innerhalb der Organisation anzugehen. Doch selbst wenn gute API-Guidelines existieren, sind diese nicht immer allen Teams bekannt. Automatisierte Validierungen von Spezifikationen können hierbei helfen und sind eine sinnvolle Ergänzung der API-Tests. Ein mögliches Tooling, mit dem man das erreichen kann, ist Stoplight Spectral [2].
Es handelt sich dabei nach eigener Definition um ein "Open Source API Styleguide Enforcer & Linter" und formuliert die Ziele:
- Verbesserte Qualität der API-Beschreibungen & -Spezifikationen
- Enforcing von API-Styleguides, wie z. B. Namenskonventionen, Vorgaben zu erlaubten oder verbotenen Datentypen usw.
Nutzbar ist Stoplight Spectral sowohl als CLI als auch als Plattform mit einer eigenen UI und Prozessunterstützung, zum Beispiel zum Teilen von Styleguides innerhalb einer Organisation. Der Einstieg ist relativ leicht möglich: Zum Start kann man sich an Best Practices von anderen Nutzern orientieren, die ihre Styleguides und Verifikationen dazu veröffentlicht haben.
In der Benutzung gibt es viele Parallelen zu anderen Tools zur Qualitätsmessung wie Sonarqube: Es gibt veröffentlichte Styleguides, genauso kann aber auch jedes Team eigene individuelle Styleguides definieren. Für jede Regel kann man Severity Level individuell festlegen, kann Regeln vererben oder überschreiben. Dabei umfassen die Regelsets verschiedene Aspekte: rein "optische" Konventionen wie die Festlegung auf Kebap Case oder Camel Case für bestimmte Parameter oder Attribute, aber auch Security-Aspekte und -Vorgaben dazu, wie Authentifizierung und Autorisierung in APIs umgesetzt werden sollen.
Ein Abschnitt für die Konvention "Paths should be kebap case." kann zum Beispiel so aussehen [3]:
rules:
paths-kebab-case:
description: Paths should be kebab-case.
message: "{{property}} should be kebab-case (lower-case and separated with hyphens)"
severity: warn
given: "#Paths"
then:
function: pattern
functionOptions:
match: "^(\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$
Wie auch für die Messung von anderen Aspekten von Software-Qualität gilt auch hier die klare Empfehlung: Wenn die Qualität der API- Spezifikationen im Fokus bleiben soll und ein Tool zur Messung dieser Qualität eingesetzt wird, sollte dies unbedingt Teil der CI/CD-Pipelines sein. Was nicht automatisiert und kontinuierlich gemessen wird, tritt schnell in den Hintergrund. Dies wird insbesondere ergänzt durch ein Alerting im Fall von Verletzungen und braucht das Commitment der Teams, diese zu beheben.
Mit einer vernünftigen Integration in die Pipelines und ausreichendem Fokus im Fall von Verletzungen sind Verifikationen der API-Spezifikationen eine sehr sinnvolle und leichtgewichtige Ergänzung der API-Tests und leisten ihren Beitrag zur Gesamtqualität der Software und ihrer APIs.
Contract-Testing
Mit Contract-Testing können wir unsere API-Tests zusätzlich sinnvoll ergänzen. Ziel dieser Tests ist es, ein gemeinsames Verständnis der Schnittstelle aus Sicht des Aufrufers (Consumer) und Anbieters (Provider) sicherzustellen und die Einhaltung von diesem in einer Art Vertrag/Kontrakt festzuhalten. Gut integriert kann dies auch klassische, manuell verwaltete Schnittstellen-Kontrakte überflüssig machen.
Contract-Tests können zum Beispiel mit Pact erstellt werden. Der Ablauf ist dabei üblicherweise der folgende [4]:
- Der API-Consumer schreibt einen Unit-Test gegen einen Mock, in diesem Fall einen Pact-Mock.
- Daraus entsteht der Pact, also der Kontrakt, der die Erwartung des Consumers beschreibt.
- Der wird dann dem Provider übermittelt (z. B. über PactFlow oder auch über andere individuelle Mechanismen).
- Der API-Provider führt dann wiederum einen Pact-Test aus, der verifiziert, dass alle Erwartungen des Consumers erfüllt werden.
Contract-Tests bieten vor allem mit Blick auf die Qualitätsaspekte "Maintainability" und "Compatibility" einen großen Mehrwert: Eine gute Test-Suite aus Contract-Tests bietet die nötige Sicherheit, um auch bei Änderungen an APIs Inkompatibilitäten auszuschließen. Durch die automatisierte Ausführung dieser Tests hält man außerdem die Kontrakte zwischen Consumern und Providern aktuell und hat einen guten Überblick über tatsächliche Schnittstellen-Aufrufe.
Dabei gilt es, folgende Aspekte zu beachten:
- Team Culture First – Contract-Tests sind kein Wundermittel für einen guten Austausch zwischen den verschiedenen Teams und tragen nicht per se zu einer guten Kommunikation bei.
- Think Big – Einen großen Mehrwert bieten Contract-Tests vor allem dann, wenn sie umfassend eingesetzt werden. Erst, wenn alle Consumer einer API ihre Contract-Tests zuliefern, kann man auf Provider-Seite mit genug Konfidenz Änderungen durchführen.
- Do not just mock again – Je nach Test-Setup sind eventuell schon verschiedene Mock-Frameworks im Einsatz. Aus Sicht des Consumers kann dies durchaus dazu führen, dass der x-te Mock für dieselbe API verwaltet werden muss. Mit Blick auf die zusätzliche Sicherheit, dass inkompatible API-Änderungen vermieden werden, sollte dieser Invest abgewogen werden.
Trace-Testing
Ein weiterer Ansatz, die Test-Suite für unsere APIs zu ergänzen, sind trace-basierte Tests wie Tracetest [5]. Dahinter steckt der Ansatz, die ohnehin als Teil des Observability-Stacks gesammelten Traces unserer Anwendungen auch für Tests und Verifikationen zu nutzen. Diese Traces liegen üblicherweise ohnehin in den verschiedenen Stages vor, so dass sowohl während der Entwicklung als auch im laufenden Betrieb Verifikationen auf den Traces möglich sind.
Tracetest erlaubt dabei die Integration verschiedener Data Stores oder alternativ auch das direkte Senden der Traces an Tracetest und bietet Integrationsmöglichkeiten über eine CLI oder auch in CI/CD-Pipelines. Die Tracetest-UI hilft je nach Einsatzzweck zusätzlich auch zur Visualisierung von Traces und unterstützt beim einfachen Erstellen der Test-Suite.
Inhaltlich sind mit Tracetest verschiedene Verifikationen auf Basis der Traces möglich, wie zum Beispiel:
- Fachliche Assertions auf Basis der API-Responses oder Traces
- Assertions zu nicht-funktionalen Anforderungen wie zum Beispiel Response-Zeiten von http-Requests oder Datenbank-Requests
- Use Case übergreifende Assertions, wie "alle http-Requests werden mit Status Code 200 beantwortet".
Vor allem die einfache Integration solcher nicht-funktionalen Aspekte hat einen großen Charme: Es fällt leicht, solche Aspekte direkt mit in die Integrations- oder End-to-End-Test-Suite einzugliedern, da sich das sehr natürlich anfühlt. Gleichzeitig braucht es auch hier Disziplin bei der Erstellung der Tests: Es passiert sonst leicht, dass sehr detailliert die einzelnen Attribute der Response mit erwarteten Ergebnissen verglichen werden, womit dieselben Nachteile wie bei klassischen API-Testaufrufen entstehen.
Richtig eingesetzt kann durch Tracetest aber die Notwendigkeit von Mocks für API-Tests deutlich in den Hintergrund treten: Werden die Tests in einer Integrationsumgebung durchgeführt, in der verlässliche Schnittstellensysteme zur Verfügung stehen, werden APIs so direkt in der Interaktion mit ihrem Umfeld abgesichert. Insbesondere dann, wenn die Traces ohnehin schon im benötigten Format vorliegen, ist die Integration sehr leichtgewichtig möglich.
Zusammenfassung
Alle drei vorgestellten Testkonzepte beziehungsweise Tools bieten eine sinnvolle Ergänzung von API-Test-Suites. Natürlich gilt es dabei je nach fachlichem und technischem Kontext, Team- und Organisationsstruktur und auch bereits bestehendem Test-Stack zu bewerten, wie spürbar der Mehrwert jeweils ist sowie an welcher Stelle und wie signifikant der Bedarf nach einer solchen Ergänzung ist.