Kontinuierliches Testen mit Microservices (mit Beispielen)
Wann ist Microservice-Architektur der richtige Weg?
Die Auswahl und Gestaltung der richtigen Architektur für ein System ist von entscheidender Bedeutung. Man muss die Anforderungen an die Servicequalität und den Umgang mit nichtfunktionalen Anforderungen wie Wartbarkeit, Erweiterbarkeit und Testbarkeit sicherstellen.
Microservice-Architektur ist in den neuesten Ökosystemen eine häufige Wahl, nachdem Unternehmen Agile und DevOps eingeführt haben. Obwohl es sich de facto nicht um eine Option handelt, ist es eine der bevorzugten Optionen, wenn es um Systeme geht, die stark wachsen und bei denen eine monolithische Architektur nicht mehr aufrechterhalten werden kann. Die serviceorientierte und lose Kopplung der Komponenten ermöglicht kontinuierliche Entwicklungs- und Veröffentlichungszyklen. Dies treibt Unternehmen dazu, ihre Software ständig zu testen und zu aktualisieren.
Die wesentlichen Voraussetzungen, die eine solche Architektur erfordern, sind:
- Domänengesteuertes Design
- Continuous Delivery und DevOps-Kultur
- Fehlerisolierung
- Dezentralisierung
Es hat folgende Vorteile:
- Teamverantwortung
- Häufige Veröffentlichungen
- Einfachere Wartung
- Einfachere Upgrades auf neuere Versionen
- Technologieunabhängig
Es hat folgende Nachteile:
- Mikroservice-zu-Mikroservice-Kommunikationsmechanismen
- Mit zunehmender Anzahl an Diensten erhöht sich die Komplexität des Gesamtsystems
Je verteilter und komplexer die Architektur ist, desto schwieriger ist es, sicherzustellen, dass das System erweitert und gewartet werden kann und gleichzeitig Kosten und Risiken kontrolliert werden. Eine Geschäftstransaktion kann mehrere Kombinationen von Protokollen und Technologien umfassen. Es geht nicht nur um die Anwendungsfälle, sondern auch um die Funktionsweise. Bei der Einführung von Agile- und DevOps-Ansätzen sollte man ein Gleichgewicht zwischen Flexibilität und Funktionalität finden, um eine kontinuierliche Überarbeitung und Prüfung zu erreichen.

Die Bedeutung von Teststrategien in Bezug auf Microservices
Die Einführung von DevOps in einer Organisation zielt darauf ab, die verschiedenen isolierten Abteilungen zu eliminieren und zu einem Gesamtteam zu gelangen. Durch diesen Schritt sollen gezielt die Beziehungen und Prozesse zwischen dem Software-Team und dem Operations-Team verbessert werden. Eine schnellere Bereitstellung bedeutet auch, dass kontinuierliche Tests als Teil der Softwarebereitstellungspipeline gewährleistet sind. Die tägliche Bereitstellung (und in manchen Fällen sogar alle paar Stunden) ist eines der Hauptziele für eine schnelle Bereitstellung von End-to-End-Geschäftslösungen. Zuverlässigkeit und Sicherheit müssen hier im Auge behalten werden, und hier kommt das Testen ins Spiel.
Die Einbeziehung der testgetriebenen Entwicklung ist die einzige Möglichkeit, echtes Vertrauen zu erlangen, dass der Code produktionsbereit ist. Gültige Testfälle steigern den Wert des Systems, da sie das System selbst validieren und dokumentieren. Darüber hinaus fördert eine gute Codeabdeckung Verbesserungen und unterstützt beim Refactoring.
Die Microservices-Architektur dezentralisiert Kommunikationskanäle, was das Testen komplizierter macht. Es ist kein unüberwindbares Problem. Ein Team, das einen Microservice besitzt, sollte keine Angst davor haben, Änderungen einzuführen, da diese bestehende Clientanwendungen beschädigen könnten. Manuelle Tests sind sehr ineffizient, wenn man bedenkt, dass kontinuierliche Integration und kontinuierliche Bereitstellung derzeit die beste Vorgehensweise sind. DevOps-Ingenieure sollten sicherstellen, dass Automatisierungstests in ihren Entwicklungsworkflow einbezogen werden: Tests schreiben, Code hinzufügen/umgestalten und Tests ausführen.
Gängige Microservice-Testmethoden
Die Testpyramide ist ein einfaches Konzept, das uns hilft, den Aufwand zu ermitteln, der beim Schreiben von Tests erforderlich ist, und anzugeben, wo die Anzahl der Tests abnehmen sollte, wenn die Granularität abnimmt. Dies gilt auch, wenn kontinuierliche Tests für Microservices in Betracht gezogen werden.

Um das Thema konkreter zu machen, werden wir uns mit dem Testen eines Beispiel-Microservices mit Spring Boot und Java befassen. Microservice-Architekturen sind konstruktionsbedingt komplizierter als monolithische Architekturen. Dennoch werden wir den Fokus weiterhin auf die Art der Tests und nicht auf die Architektur legen. Unsere Snippets basieren auf einem Minimalprojekt, das aus einem API-gesteuerten Mikrodienst besteht, der einen Datenspeicher unter Verwendung von MongoDB besitzt.
Unit-Tests
Unit-Tests sollten den Großteil der Tests ausmachen, da sie schnell, zuverlässig und einfach zu warten sind. Diese Tests werden auch White-Box-Tests genannt. Der Ingenieur, der sie implementiert, ist mit der Logik vertraut und schreibt den Test, um die Modulspezifikationen zu validieren und die Qualität des Codes zu überprüfen.
Der Schwerpunkt dieser Tests liegt auf einem kleinen isolierten Teil des Systems, nämlich der zu testenden Klasse (Class Under Test, CUT). Das Single-Responsibility-Prinzip ist eine gute Richtlinie für die Verwaltung von Code im Hinblick auf die Funktionalität.
Die häufigste Form eines Unit-Tests ist ein „Einzel-Unit-Test“. „Es überschreitet keine Grenzen und benötigt keine Mitarbeiter außer dem CUT.“
Wie von Bill Caputo dargelegt, sind Datenbanken, Messaging-Kanäle oder andere Systeme die Grenzen; Jede zusätzliche Klasse, die vom CUT verwendet oder benötigt wird, ist ein Mitarbeiter. Ein Unit-Test sollte niemals eine Grenze überschreiten. Wenn man Mitarbeiter einsetzt, schreibt man einen „geselligen Unit-Test“. „Die Verwendung von Mocks für Abhängigkeiten, die vom CUT verwendet werden, ist eine Möglichkeit, geselligen Code mit einem „einzelnen Unit-Test“ zu testen. ”
In herkömmlichen Softwareentwicklungsmodellen waren Entwicklertests noch nicht weit verbreitet, da die Tests völlig unabhängig von der Entwicklung durchgeführt werden mussten. Das Erreichen einer hohen Codeabdeckungsbewertung galt als Schlüsselindikator für die Zuverlässigkeit der Testsuite.
Mit der Einführung von Agile und kurzen Iterationszyklen zeigt sich nun, dass bisherige Testmodelle nicht mehr funktionieren. Es ist laufend mit häufigen Änderungen zu rechnen. Es ist viel wichtiger, beobachtbares Verhalten zu testen, als alle Codepfade abzudecken. Bei Unit-Tests sollte es mehr um Behauptungen als um Codeabdeckung gehen, da das Ziel darin besteht, zu überprüfen, ob die Logik wie erwartet funktioniert.
Es ist sinnlos, eine Komponente mit vielen Tests und einem hohen Prozentsatz an Codeabdeckung zu haben, wenn Tests keine richtigen Behauptungen haben. Durch die Anwendung eines eher verhaltensgesteuerten Entwicklungsansatzes (BDD) wird sichergestellt, dass Tests den Endzustand überprüfen und dass das Verhalten den vom Unternehmen festgelegten Anforderungen entspricht. Der Vorteil fokussierter Tests mit einem klar definierten Umfang besteht darin, dass die Fehlerursache leichter ermittelt werden kann. BDD-Tests geben uns eine höhere Sicherheit, dass der Fehler eine Folge einer Änderung im Funktionsverhalten war. Tests, die sich sonst mehr auf die Codeabdeckung konzentrieren, können keine große Sicherheit bieten, da das Risiko größer wäre, dass Fehler eine Auswirkung auf Änderungen sind, die aufgrund von Implementierungsdetails der Codepfade in den Tests selbst vorgenommen wurden.
Tests sollten dem Vorschlag von Martin Fowler folgen, der Folgendes feststellte (in Refactoring: Improving the Design of Existing Code, Second Edition. Kent Beck und Martin Fowler. Addison-Wesley. 2018):

Ein weiterer Grund, sich weniger auf kleinere Implementierungsdetails zu konzentrieren, ist das Refactoring. Während des Refactorings sollten Unit-Tests dazu dienen, uns Sicherheit zu geben und die Arbeit nicht zu verlangsamen. Eine Änderung in der Implementierung des Collaborators kann zu einem Testfehler führen, was die Wartung der Tests erschweren könnte. Es wird dringend empfohlen, ein Minimum an geselligen Unit-Tests einzuhalten. Dies gilt insbesondere dann, wenn solche Tests den Entwicklungslebenszyklus verlangsamen könnten und die Möglichkeit besteht, dass Tests letztendlich ignoriert werden. Ein hervorragender Fall für die Einbeziehung eines geselligen Unit-Tests sind Negativtests, insbesondere wenn es um die Verhaltensüberprüfung geht.
Integrationstests
Eine der größten Herausforderungen bei Microservices besteht darin, ihre Interaktion mit den übrigen Infrastrukturdiensten zu testen, d. h. die Grenzen, von denen der jeweilige CUT abhängt, wie etwa Datenbanken oder andere Dienste. Die Testpyramide zeigt deutlich, dass Integrationstests weniger als Unit-Tests, aber mehr als Komponenten- und End-to-End-Tests sein sollten. Diese anderen Arten von Tests sind möglicherweise langsamer, schwieriger zu schreiben und zu warten und im Vergleich zu Komponententests recht anfällig. Das Überschreiten von Grenzen kann sich aufgrund des Netzwerk- und Datenbankzugriffs auf die Leistung und die Ausführungszeit auswirken. Dennoch sind sie insbesondere in der DevOps-Kultur unverzichtbar.
Im Rahmen einer kontinuierlichen Bereitstellung werden enge Integrationstests gegenüber breiten Integrationstests bevorzugt. Letzteres kommt End-to-End-Tests sehr nahe, bei denen der tatsächliche Dienst ausgeführt werden muss und nicht ein Testdoppel dieser Dienste zum Testen der Codeinteraktionen verwendet werden muss. Das Hauptziel besteht darin, überschaubare operative Tests schnell, einfach und belastbar zu erstellen. Integrationstests konzentrieren sich auf die Interaktion des CUT mit jeweils einem Dienst. Unser Fokus liegt auf engen Integrationstests. Die Überprüfung der Interaktion zwischen einem Paar von Diensten kann als erwartet bestätigt werden, wobei es sich bei den Diensten entweder um einen Infrastrukturdienst oder einen anderen Dienst handeln kann.
Persistenztests
>Eine umstrittene Testart ist das Testen der Persistenzschicht, mit dem primären Ziel, die Abfragen und die Auswirkung auf Testdaten zu testen. Eine Möglichkeit ist die Nutzung von In-Memory-Datenbanken. Einige könnten die Verwendung von In-Memory-Datenbanken als einen geselligen Unit-Test betrachten, da es sich um einen eigenständigen Test handelt, der idempotent und schnell ist. Der Test wird mit der Datenbank ausgeführt, die mit der gewünschten Konfiguration erstellt wurde. Nachdem die Testläufe und Behauptungen überprüft wurden, wird der Datenspeicher aufgrund seiner kurzlebigen Natur automatisch bereinigt, sobald die JVM beendet wird. Beachten Sie, dass immer noch eine Verbindung zu einem anderen Dienst besteht und dies als enger Integrationstest angesehen wird. Bei einem Test-Driven-Development-Ansatz (TDD) sind solche Tests unerlässlich, da Testsuiten innerhalb von Sekunden ausgeführt werden sollten. In-Memory-Datenbanken sind ein sinnvoller Kompromiss, um sicherzustellen, dass Tests so schnell wie möglich durchgeführt und auf lange Sicht nicht ignoriert werden.
@Before
public void setup() throws Exception {
try {
// this will download the version of mongo marked as production. One should
// always mention the version that is currently being used by the SUT
String ip = "localhost";
int port = 27017;
IMongodConfig mongodConfig = new MongodConfigBuilder().version(Version.Main. PRODUCTION)
.net(new Net(ip, port, Network.localhostIsIPv6())).build();
MongodStarter starter = MongodStarter.getDefaultInstance();
mongodExecutable = starter.prepare(mongodConfig);
mongodExecutable.start();
} catch (IOException e) {
e.printStackTrace();
}
}Snippet 1: Installation und Start der In-Memory MongoDB
Das Obige ist kein vollständiger Integrationstest, da sich eine In-Memory-Datenbank nicht genau wie der Produktionsdatenbankserver verhält. Daher handelt es sich nicht um eine Nachbildung des „echten“ Mongo-Servers, was der Fall wäre, wenn man sich für umfassende Integrationstests entscheidet.
Eine weitere Option für Persistenz-Integrationstests besteht darin, umfassende Tests in Verbindung mit einem tatsächlichen Datenbankserver oder unter Verwendung von Containern auszuführen. Container erleichtern den Aufwand, da man die Datenbank auf Anfrage bereitstellt, im Vergleich zu einem festen Server. Bedenken Sie, dass solche Tests zeitaufwändig sind und die Kategorisierung von Tests eine mögliche Lösung darstellt. Da diese Tests davon abhängen, dass neben dem CUT ein anderer Dienst ausgeführt wird, handelt es sich um einen Systemtest. Diese Tests sind immer noch unerlässlich, und durch die Verwendung von Kategorien kann man besser bestimmen, wann bestimmte Tests durchgeführt werden sollten, um das beste Gleichgewicht zwischen Kosten und Nutzen zu erzielen. Beispielsweise könnte man während des Entwicklungszyklus nur die engen Integrationstests unter Verwendung der In-Memory-Datenbank ausführen. Nightly Builds könnten auch Tests ausführen, die unter eine Kategorie wie umfassende Integrationstests fallen.
@Category(FastIntegration.class)
@RunWith(SpringRunner.class)
@DataMongoTest
public class DailyTaskRepositoryInMemoryIntegrationTest {
. . .
}
@Category(SlowIntegration.class)
@RunWith(SpringRunner.class)
@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)
public class DailyTaskRepositoryIntegrationTest {
...
}Snippet 2: Verwenden von Kategorien zur Unterscheidung der Arten von Integrationstests
Verbraucherorientierte Tests
Inter-Process Communication (IPC)-Mechanismen sind ein zentraler Aspekt verteilter Systeme, die auf einer Microservices-Architektur basieren. Dieser Aufbau führt zu verschiedenen Komplikationen bei der Erstellung von Testsuiten. Darüber hinaus werden in einem agilen Team ständig Änderungen vorgenommen, einschließlich Änderungen an APIs oder Ereignissen. Unabhängig davon, welchen IPC-Mechanismus das System verwendet, besteht ein Vertrag zwischen zwei beliebigen Diensten. Es gibt verschiedene Arten von Verträgen, je nachdem, welchen Mechanismus man im System nutzt. Bei der Verwendung von APIs ist der Vertrag die HTTP-Anfrage und -Antwort, während im Fall eines ereignisbasierten Systems der Vertrag das Domänenereignis selbst ist.
Ein Hauptziel beim Testen von Microservices besteht darin, sicherzustellen, dass diese Verträge zu jedem Zeitpunkt klar definiert und stabil sind. Bei einem TDD-Top-Down-Ansatz sind dies die ersten Tests, die abgedeckt werden. Ein grundlegender Integrationstest stellt sicher, dass der Verbraucher eine schnelle Rückmeldung erhält, sobald ein Kunde nicht mit dem tatsächlichen Zustand des Produzenten übereinstimmt, mit dem er spricht.
Diese Tests sollten Teil der regulären Bereitstellungspipeline sein. Ihr Scheitern würde es den Verbrauchern ermöglichen, sich bewusst zu machen, dass eine Änderung auf der Produzentenseite stattgefunden hat und dass Änderungen erforderlich sind, um wieder Konsistenz zu erreichen. Ohne die Notwendigkeit, komplizierte End-to-End-Tests zu schreiben, würden „verbrauchergesteuerte Vertragstests“ auf diesen Anwendungsfall abzielen.
Das Folgende ist ein Beispiel eines Vertragsprüfers, der vom Spring-Cloud-Contract-Plugin generiert wird.
@Test
public void validate_add_New_Task() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json;charset=UTF-8")
.body("{"taskName":"newTask","taskDescription":"newDescription","isComplete":false,"isUrgent":true}");
// when:
ResponseOptions response = given().spec(request).post("/tasks");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['taskName']").isEqualTo("newTask");
assertThatJson(parsedJson).field("['isUrgent']").isEqualTo(true);
assertThatJson(parsedJson).field("['isComplete']").isEqualTo(false);
assertThatJson(parsedJson).field("['id']").isEqualTo("3");
assertThatJson(parsedJson).field("['taskDescription']").isEqualTo("newDescription");
}Snippet 3: Vertragsüberprüfung, automatisch generiert durch das Spring-Cloud-Contract-Plugin
Eine im Produzenten geschriebene BaseClass gibt an, welche Art von Antwort auf die verschiedenen Arten von Anfragen zu erwarten ist, indem das Standalone-Setup verwendet wird. Die gepackte Sammlung von Stubs steht allen Verbrauchern zur Verfügung, um sie in ihrer Implementierung abrufen zu können. Komplexität entsteht, wenn mehrere Verbraucher denselben Vertrag nutzen; Daher muss der Hersteller einen Gesamtüberblick über die erforderlichen Serviceverträge haben.
@RunWith(SpringRunner.class)
@SpringBootTest
public class ContractBaseClass {
@Autowired
private DailyTaskController taskController;
@MockBean
private DailyTaskRepository dailyTaskRepository;
@Before
public void before() {
RestAssuredMockMvc.standaloneSetup(this.taskController);
Mockito.when(this.dailyTaskRepository.findById("1")).thenReturn(
Optional.of(new DailyTask("1", "Test", "Description", false, null)));
. . .
Mockito.when(this.dailyTaskRepository.save(
new DailyTask(null, "newTask", "newDescription", false, true))).thenReturn(
new DailyTask("3", "newTask", "newDescription", false, true));
}Snippet 4: Die BaseClass des Produzenten, die die für jede Anfrage erwartete Antwort definiert
Auf der Verbraucherseite haben wir den Test unter Einbeziehung der Spring-Cloud-Starter-Contract-Stub-Runner-Abhängigkeit für die Verwendung der Stubs-Binärdatei konfiguriert. Dieser Test würde unter Verwendung der Stubs ausgeführt werden, die vom Produzenten gemäß der Konfiguration mit der angegebenen Version oder immer der neuesten Version generiert wurden. Das Stub-Artefakt verbindet den Client mit dem Produzenten, um sicherzustellen, dass beide am selben Vertrag arbeiten. Jede auftretende Änderung würde sich in diesen Tests widerspiegeln, und so würde der Verbraucher erkennen, ob der Hersteller Änderungen vorgenommen hat oder nicht.
@SpringBootTest(classes = TodayAskApplication.class)
@RunWith(SpringRunner.class)
@AutoConfigureStubRunner(ids = "com.cwie.arch:today:+:stubs:8080", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class TodayClientStubTest {
. . .
@Test
public void addTask_expectNewTaskResponse () {
Task newTask = todayClient.createTask(
new Task(null, "newTask", "newDescription", false, true));
BDDAssertions.then(newTask).isNotNull();
BDDAssertions.then(newTask.getId()).isEqualTo("3");
. . .
}
}Snippet 5: Verbraucher fügt die vom Produzenten definierte Stub-Version ein
Solche Integrationstests überprüfen, ob die API eines Anbieters weiterhin den Erwartungen der Verbraucher entspricht. Bei der Verwendung simulierter Unit-Tests für APIs hätten wir APIs abgeblockt und das Verhalten simuliert. Aus Verbrauchersicht stellen solche Tests sicher, dass der Kunde unseren Erwartungen entspricht. Es ist wichtig zu beachten, dass diese Tests nicht fehlschlagen, wenn die Herstellerseite die API ändert. Und es ist unbedingt zu definieren, was der Test abdeckt.
// the response we expect is represented in the task1.json file
private Resource taskOne = new ClassPathResource("task1.json");
@Autowired
private TodayClient todayClient;
@Test
public void createNewTask_expectTaskIsCreated() {
WireMock.stubFor(WireMock.post(WireMock.urlMatching("/tasks"))
.willReturn(WireMock.aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
.withStatus(HttpStatus.OK.value())
.withBody(transformResourceJsonToString(taskOne))));
Task tasks = todayClient.createTask(new Task(null, "runUTest", "Run Test", false, true));
BDDAssertions.then(tasks.getId()).isEqualTo("1");Snippet 6: Ein Verbrauchertest, der Behauptungen zu seiner eigenen definierten Antwort durchführt
Komponententests
Die Microservice-Architektur kann schnell wachsen, sodass die zu testende Komponente möglicherweise in mehrere andere Komponenten und mehrere Infrastrukturdienste integriert werden kann. Bisher haben wir White-Box-Tests mit Unit-Tests und engen Integrationstests abgedeckt, um zu testen, ob der CUT die Grenze zur Integration mit einem anderen Dienst überschreitet.
Die schnellste Art des Komponententests ist der In-Process-Ansatz, bei dem neben der Verwendung von Testdoppeln und In-Memory-Datenspeichern das Testen in Grenzen bleibt. Der Hauptnachteil dieses Ansatzes besteht darin, dass der bereitstellbare Produktionsdienst nicht vollständig getestet wird; im Gegenteil, die Komponente erfordert Änderungen, um die Anwendung anders zu vernetzen. Die bevorzugte Methode ist das Testen von Komponenten außerhalb des Prozesses. Dabei handelt es sich um End-to-End-Tests, bei denen jedoch alle externen Mitarbeiter durch Test-Doubles ersetzt werden. Auf diese Weise wird das vollständig bereitgestellte Artefakt unter Verwendung echter Netzwerkaufrufe getestet. Der Test wäre dafür verantwortlich, alle externen Dienste ordnungsgemäß als Stubs zu konfigurieren.
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { TodayConfiguration.class, TodayIntegrationApplication.class,
CloudFoundryClientConfiguration.class })
public class BaseFunctionalitySteps {
@Autowired
private CloudFoundryOperations cf;
private static File manifest = new File(".manifest.yml");
@Autowired
private TodayClient client;
// Any stubs required
. . .
public void setup() {
cf.applications().pushManifest(PushApplicationManifestRequest.builder()
.manifest(ApplicationManifestUtils.read(manifest.toPath()).get(0)).build()).block();
}
. . .
// Any calls required by tests
public void requestForAllTasks() {
this.client.getTodoTasks();
}
}Snippet 7: Bereitstellung des Manifests auf CloudFoundry und aller für Tests erforderlichen Aufrufe
Cloud Foundry ist eine der Optionen für Container-basierte Testarchitekturen. „Es handelt sich um eine Open-Source-Cloud-Anwendungsplattform, die das Erstellen, Testen, Bereitstellen und Skalieren von Anwendungen schneller und einfacher macht. „Das Folgende ist die Datei manifest.yml, die die Konfiguration aller Anwendungen im System definiert. Diese Datei wird verwendet, um den eigentlichen Dienst im produktionsbereiten Format im Bereich der Pivotal-Organisation bereitzustellen, wo der MongoDB-Dienst bereits eingerichtet ist, passend zur Produktionsversion.
---
applications:
- name: today
instances: 1
path: ../today/target/today-0.0.1-SNAPSHOT.jar
memory: 1024M
routes:
- route: today.cfapps.io
services:
- mongo-itSnippet 8: Bereitstellung einer Instanz des Dienstes abhängig vom Mongo-Dienst
Wenn Sie sich für den Out-of-Process-Ansatz entscheiden, bedenken Sie, dass tatsächliche Grenzen getestet werden und die Tests daher letztendlich langsamer sind, da Netzwerk- und Datenbankinteraktionen stattfinden. Ideal wäre es, diese Testsuiten in einem separaten Modul zu schreiben. Um sie separat in einer anderen Maven-Phase ausführen zu können, anstatt in der üblichen „Test“-Phase.
Da der Schwerpunkt der Tests auf der Komponente selbst liegt, decken Tests die Hauptaufgaben der Komponente ab, während andere Teile des Systems absichtlich vernachlässigt werden.
Cucumber, ein Softwaretool, das Behavior-Driven Development unterstützt, ist eine Option zur Definition solcher Verhaltenstests. Mit seinem Klartext-Parser Gherkin sorgt es dafür, dass Kunden alle beschriebenen Tests leicht verstehen können. Die folgende Cucumber-Funktionsdatei stellt sicher, dass unsere Komponentenimplementierung den Geschäftsanforderungen für diese bestimmte Funktion entspricht.
Feature: Tasks
Scenario: Retrieving one task from list
Given the component is running
And the data consists of one or more tasks
When user requests for task x
Then the correct task x is returned
Scenario: Retrieving all lists
Given the data consists of one or more tasks
When user requests for all tasks
Then all tasks in database are returned
Scenario: Negative Test
Given the component is not running
When user requests for task x it fails with response 404Snippet 9: Eine Feature-Datei, die BDD-Tests definiert
End-to-End-Tests
Ähnlich wie bei Komponententests besteht das Ziel dieser End-to-End-Tests nicht darin, eine Codeabdeckung durchzuführen, sondern sicherzustellen, dass das System die geforderten Geschäftsszenarien erfüllt. Der Unterschied besteht darin, dass beim End-to-End-Test alle Komponenten während des Tests betriebsbereit sind.
Gemäß dem Testpyramidendiagramm nimmt die Anzahl der End-to-End-Tests unter Berücksichtigung der dadurch möglicherweise verursachten Langsamkeit weiter ab. Der erste Schritt besteht darin, das Setup auszuführen. In diesem Beispiel verwenden wir docker.
version: '3.7'
services:
today-app:
image: today-app:1
container_name: "today-app"
build:
context: ./
dockerfile: DockerFile
environment:
- SPRING_DATA_MONGODB_HOST=mongodb
volumes:
- /data/today-app
ports:
- "8082:8080"
links:
- mongodb
depends_on:
- mongodb
mongodb:
image: mongo:3.2
container_name: "mongodb"
restart: always
environment:
- AUTH=no
- MONGO_DATA_DIR=/data/db
- MONGO_LOG_DIR=/dev/log
volumes:
- ./data:/data
ports:
- 27017:27017
command: mongod --smallfiles --logpath=/dev/null # --quietSnippet 10: Die docker.yml-Definition, die zum Bereitstellen des definierten Dienstes und der angegebenen Version von Mongo als Container verwendet wird
Bei Komponententests ist es sinnvoll, End-to-End-Tests in einem separaten Modul und in verschiedenen Phasen durchzuführen. Das exec-maven-plugin wurde verwendet, um alle erforderlichen Komponenten bereitzustellen, unsere Tests auszuführen und schließlich unsere Testumgebung zu bereinigen und herunterzufahren.

Da es sich um einen Broad-Stack-Test handelt, wird eine kleinere Auswahl an Tests pro Feature ausgeführt. Die Tests werden auf der Grundlage des wahrgenommenen Geschäftsrisikos ausgewählt. Die vorherigen Arten von Tests deckten Details auf niedriger Ebene ab. Das bedeutet, ob eine User Story den Akzeptanzkriterien entspricht. Auch bei diesen Tests sollte eine Veröffentlichung sofort gestoppt werden, da ein Fehler hier schwerwiegende geschäftliche Auswirkungen haben könnte.
Abschluss
Übergabezentrierte Tests sind oft ein sehr langwieriger Prozess, der mehrere Wochen dauern kann, bis alle Fehler identifiziert, behoben und eine neue Bereitstellung vorbereitet sind. Feedback erhalten wir erst, nachdem eine Veröffentlichung erfolgt ist, wodurch die Lebensdauer einer Version zu unserer schnellstmöglichen Bearbeitungszeit wird.
Der kontinuierliche Testansatz sorgt für sofortiges Feedback. Das bedeutet, dass der DevOps-Ingenieur je nach Ergebnis des Testlaufs sofort weiß, ob die implementierte Funktion produktionsbereit ist oder nicht. Von Unit-Tests bis hin zu End-to-End-Tests tragen sie alle dazu bei, den Bewertungsprozess zu beschleunigen.
Die Microservices-Architektur trägt zu einer schnelleren Einführung in die Produktion bei, da sie domänengesteuert ist. Es stellt die Fehlerisolierung sicher und erhöht die Eigenverantwortung. Wenn mehrere Teams an demselben Projekt arbeiten, ist dies ein weiterer Grund für die Einführung einer solchen Architektur: Um sicherzustellen, dass die Teams unabhängig sind und sich nicht gegenseitig in ihrer Arbeit behindern.
Verbessern Sie die Testbarkeit, indem Sie auf kontinuierliche Tests umsteigen. Jeder Microservice verfügt über eine genau definierte Domäne und sein Umfang sollte auf einen Akteur beschränkt sein. Die angewandten Testfälle sind spezifischer und prägnanter, und die Tests sind isoliert, was Releases und schnellere Bereitstellungen erleichtert.
Nach dem TDD-Ansatz gibt es keine Codierung, es sei denn, ein fehlgeschlagener Test wird zurückgegeben. Dieser Prozess erhöht das Vertrauen, sobald eine iterative Implementierung zu einem erfolgreichen Versuch führt. Dieser Prozess impliziert, dass das Testen parallel zur eigentlichen Implementierung erfolgt und alle oben genannten Tests ausgeführt werden, bevor Änderungen eine Staging-Umgebung erreichen. Kontinuierliche Tests entwickeln sich weiter, bis sie in die nächste Release-Phase gelangen, also in eine Staging-Umgebung, in der der Schwerpunkt auf umfassendere Tests wie Lasttests verlagert wird.
Agile, DevOps und Continuous Delivery erfordern kontinuierliche Tests. Der Hauptvorteil ist das unmittelbare Feedback, das durch automatisierte Tests entsteht. Die möglichen Auswirkungen könnten sich auf das Benutzererlebnis auswirken, aber auch risikoreiche geschäftliche Folgen haben. Für weitere Informationen zu kontinuierlichen Tests wenden Sie sich noch heute an phoenixNAP.