Microservices - Teil 5

Testverfahren für Microservices

End-to-End-Tests

End-To-End-Tests sind Tests, welche das System als Gesamtheit testen. Für den Test von monolithischen Systemen wird die Form des Tests häufig eingesetzt, um die Funktionalität des Systems zu testen.

Für Microservice-Architekturenbringt sie aber eine Reihe von Problemen mit sich [Newman]:

  • Eine Microservice-Infrastruktur ist komplex. Ein Test der Infrastruktur erweitert den Scope des Tests über die Funktionalität der einzelne Microservices hinaus auf die gesamte Infrastruktur. Alle Probleme dieser Infrastruktur (wie Service-Downtime) werden Teil des Testscopes. Entsprechend schwer sind die Testergebnisse zu interpretieren.
    Die Wahrscheinlichkeit, dass die Tests mal erfolgreich, mal nicht erfolgreich verlaufen (sogenannte flaky test[Fowler-2]), erhöht sich. Solche Tests sind im Wesentlichen wertlos.
  • Die Verantwortlichkeit für einen Microservice ist einem Entwicklungsteam zuzuordnen. End-To-End-Tests dagegen betreffen in der Regel mehrere Teams. Dies hat zur Folge, dass die Verantwortlichkeit für diese Tests nicht mehr klar ist.
  • In der Testumgebung finden sich Microservices in unterschiedlichen Reifegraden und die Testumgebung entspricht nicht der Produktivumgebung. End-To-End-Tests laufen in einem sehr speziellen Kontext. Konsumierte Microservices unterscheiden sich häufig von der Version, welche aktuell produktiv eingesetzt wird. Unter diesen Bedingungen hängt die Aussagekraft eines End-To-End-Tests von der Testumgebung und den dort eingesetzten Versionen der Microservices Auf diese Weise schleicht sich die mühsam erlangte Unabhängigkeit der Microservices durch die Hintertür wieder ein.
  • Wird großer Wert auf End-To-End-Tests gelegt und große Teile der Funktionalität des Systems durch solche Tests abgedeckt, kann es schnell zu immensen Laufzeiten für solche Tests kommen. Diese langen Laufzeiten verringern die Fähigkeiten schnell auszuliefern.

End-To-End-Tests haben natürlich ihre Bedeutung, haben aber – gerade in großen Systemen mit vielen Microservices – ihren Preis. Sie sind aus den oben genannten Gründen nicht gut geeignet, Funktionalität zu testen. Sie sind weniger fachliches Abnahmekriterium, sondern vielmehr Indikator für die Vitalität des Systems.

Daher eigenen sie sich neben Tests für die Abnahmeumgebung als Vitalitätstest für das Produktivsystem (semantic monitoring [Newman]). Einige ausgewählte Szenarien werden gegen das produktive System ausgeführt und lassen so Schlüsse auf den aktuellen Zustand des Systems aus fachlicher Sicht zu.

Im Gegensatz zum herkömmlichem Systemmonitoring, welches Systemparameter wie CPU-Verbrauch, Antwortzeiten oder Filesysteme-Größe prüft, prüft diese Form von Monitoring den fachlichen Zustand des Systems. Ergebnisses dieses Monitoring sollten natürlich in Monitoringtools wie Nagios eingespeist werden.

Contract Testing

Microservices werden im herkömmlichen Sinne normalen Komponenten- oder Servicetests unterzogen. Diese Servicetests müssen den Zugriff auf konsumierte Microservices in irgendeiner Weise simulieren. Es werden die öffentlichen Schnittstellen der Microservices getestet. Testverfahren müssen die Microservices isoliert testen können. Dazu muss eine Umgebung geschaffen werden, in der

  • der Consumer des zu testenden Microservices ihre Testanforderungen gegen die öffentliche Schnittstelle des Microservices prüfen können.
  • Microservices, welche durch den zu testenden Microservice ihrerseits konsumiert werden, simuliert werden können.

Dazu formuliert der Consumer eines Microservices aus seiner Sicht, wie sich dieser zu testende Microservice verhalten soll (Consumer Driven Contract). Damit wird natürlich nur ein Teil der gesamten öffentlichen Schnittstelle des Microservices angesprochen. Die Summe aller Consumer Driven Contracts definiert letztendlich die Anforderung an den Microservice.

Ein Consumer Driven Contract enthält abzusetzende Anfragen und die erwarteten Antworten.

Consumer-Driven-Contract-Tests ([Fowler-3]) testen die öffentliche Schnittstelle eines Microservices aus Sicht eines Consumer Driven Contract. Es werden die Anfragen aus dem Consumer Driven Contract abgesetzt und Antworten gegen die Erwartung geprüft. Natürlich muss der Inhalt des Consumer Driven Contract mit dem tatsächlichen Verhalten des Providers abgeglichen werden. Dazu werden sogenannte Integration Contract Tests ([Fowler5]) gestartet, welche die Antworten des Providers mit den Erwartungen des Consumer Driven Contract abgleichen.

Wird ein Microservice getestet, so ist es sinnvoll die von diesem Microservice konsumierten Microservices (Provider) zu simulieren. Auch dazu lassen sich der Consumer Driven Contracts nutzen, die aus den Consumer-Driven-Contract-Tests gegen diese Provider entstanden. In diesen Contracts ist das erwartete Verhalten der Provider dokumentiert. Eine Simulation, welche auf Anfragen aus den Contracts die dort beschriebenen Antworten sendet, ist natürlich sehr gut geeignet, den Provider im Context des zu testenden Microservices zu ersetzen.

Ein Consumer Contract ermöglicht es somit, zum einen einen Provider isoliert von tatsächlichen Consumers zu testen und andererseits Consumer zu testen, ohne alle notwendigen Provider vorhalten zu müssen.

Consumer-Driven-Tests mit PACT

Tools wie [Pact] oder [Pacto]unterstützen die Beschreibung von Consumer Driven Contract auf Basis von REST/HTTP und die Ausführung von Consumer-Driven-Tests. Anhand von [Pact] soll die Funktionsweise von consumer driven testerläutert werden.

Im ersten Schritt soll die Möglichkeit geschaffen werden, einen Microservice (Consumer) zu testen, ohne die notwendigen abhängigen Services zu nutzen. Dazu wird ein sogenannter Mock des zu konsumierenden Services(Provider) implementiert (Java, Ruby, .Net,…). Dieser beschreibt den consumer contract, in welchem für den Mockservice festgelegt wird, welche Ergebnisse für welchen Request erwartet werden. Dieser Mockservice kann gestartet werden (z.B. aus gradle, maven, …). In speziellen PACT-Unittests interagiert der Consumer mit dem Mockservice und führt seine Testfälle aus. In Abbildung 1 ist dieser Testschritt blau hinterlegt.

Bei diesem Test wird ein PACT-File erzeugt. Dieser enthält die Requests, welche der Consumer abgesetzt hat, also auch der erwarteten Ergebnisse, also kurz gesagt den consumer contract.

Dieser PACT-File kann in einem nächsten Schritt genutzt werden, um den Provider zu testen (aus Sicht des Consumers, der das PACT-File erzeugt hat). Zu diesem Test braucht es wiederum keinen Consumer, sondern PACT (genauer die Komponenten pact-jvm) spielt den Inhalt den PACT-Files ab, setzt die dort definierten Requests gegen den Provider ab und prüft diese gegen die erwarteten Ergebnisse. In Abbildung 1 ist dieser Testschritt hinterlegt.

Der PACT-File repräsentiert den Consumer Contract und ist die zentrale Größe für alle oben beschrieben Testszenarios.

{ 
   "provider":
     { "name": "Animal Service" }, 
   "consumer": 
      { "name": "Zoo App" }, 
   "interactions": [ 
   { 
      "description": "a request for an alligator", 
      "provider_state": "there is an alligator named Mary", 
      "request": { 
         "method": "get", 
         "path": "/alligators/Mary", 
         "headers": { "Accept": "application/json" } 
      }, 
      "response": { 
         "status": 200, 
         "headers": { "Content‐Type": "application/json;charset=utf‐8" }, 
         "body": { "name": "Mary" } 
       } 
   }, 
   { 
    "description": . . .
   ], 
   "metadata": { 
      "pactSpecificationVersion": "1.0.0" 
   } 
}


Referenzen

[Newman] Sam Newman - Building Microservices

Jetzt teilen: