Hier entwickeln wir Microservices und IT-Systemen für die Cloud mit Java oder Kotlin. Für die einfache Nutzung von Datenbanken (z.B. MongoDB), Security- oder Frontend-Technologien stelle ich das Spring Framework mit dem Spring Boot Projekt vor. Mit Clean Code Prinzipen und Test-Automatisierungstechniken zeige ich, wie Wartbarkeit und Qualität sichergestellt wird.
Testautomatisierung von REST-APIs und Microservices
Link abrufen
Facebook
X
Pinterest
E-Mail
Andere Apps
Services oder Microservices mit REST-API können mit wenigen und einfachen Tools entwicklungsbegleitend getestet werden. Mit dem Spring Boot Standard Technologie-Stack bestehend aus JUnit, Mockito und Maven können EntwicklerInnen direkt loslegen und automatisierte Unit- und Integrationstests schreiben. In diesem Artikel werde ich dies demonstrieren und Tipps zur Integration in eine CICD Pipeline geben.
Die Testpyramide bzw. Testarten
Wenn ihr nach der Testpyramide sucht, findet ihr relativ viele verschiedene Varianten. Das Prinzip ist aber immer ähnlich:
die meisten Tests werden auf der untersten Ebene benötigt, wo die kleinsten Einheiten möglichst umfangreich getestet werden. Das ist typischer Weise die Testart Unit Tests zum Testen von einzelnen Klassen.
Je höher man in den Ebenen der Testpyramide aufsteigt, desto weniger Tests werden benötigt, da diese Tests einen deutlich größeren Bereich des Systems durchlaufen.
Für Services oder Microservices mit einer REST-API könnte die Testpyramide beispielsweise so aussehen.
Microservice Code-Schichtung
Betrachten wir einen typischen Microservice, so finden wir meistens eine Strukturierung in Schichten. Im folgenden Schaubild gibt es 3 Schichten:
Unten die Persistenz-Schicht zur Datenhaltung (meist in einer Datenbank)
In der Mitte die Business-Logik-Schicht, welche die Implementierung der Geschäftslogik des Microservices enthält
Oben die API-Schicht, welche HTTP Requests annimmt, JSON-Daten parst und evtl. validiert und dann die passende Funktion in der Business-Logik-Schicht aufruft, um die Verarbeitung zu starten.
Die zuvor gezeigt Testpyramide lässt sich nun auf die Schichten anwenden. Die Klassen oder Spring Beans in der unteren und mittleren Schicht sollten mit Unit-Tests getestet werden.
Die Klassen der API-Schicht haben im Idealfall keine Geschäftslogik. Sie nehmen nur den REST-Request an und sorgen dafür, dass der JSON-Body oder die Request-Parameter geparst und validiert als Objekte im Code der Controller-Klassen ankommen. Dort werden die Datenobjekte an die passende Funktion in der Business-Logik-Schicht weitergegeben. Wenn wir hierzu passende Bibliotheken verwenden (z.B. Jackson und Hibernate, siehe validation-with-spring.html), dann können wir hier evtl. sogar auf Unit Tests verzichten. Ungetestet dürfen unsere Controller-Klassen natürlich nicht bleiben, deshalb verwenden wir hier die Integrationstests der 2. Ebene unserer Testpyramide.
Die dritte Ebene der Testpyramide testet die Außenkante unseres Microservices. Bei den Ende zu Ende Tests (E2E) rufen wir im Idealfall Requests so auf, wie es auch die Clients unserer Microservice REST-API tun würden. Im Schaubild habe ich in dieser Rolle exemplarisch ein Beispiel Portal und ein Beispiel Service eingezeichnet.
In den folgenden Abschnitten zeige ich einen einfachen Technologie-Stack, der diese Ebenen abdecken kann. Es gibt hier zahlreiche Alternative Technologie-Stacks, die ich hier aber nicht miteinander vergleichen oder bewerten möchte.
Video zum Blog-Artikel
Unit Tests mit JUnit 5 und Mockito
JUnit ist ein weit verbreitetes, klassischen Java Framework für Unit Tests. Dabei befinden wir uns auf Klassenebene und erzeugen für jede Klasse in unserem Code eine Testklasse, welche im Idealfall den kompletten Code in dieser Klasse testet. Für generierte Modell-Klassen oder klassische Datenklassen, die nur generierte Getter- und Setter-Methoden enthalten, spare ich mir häufig die Unit Tests, wenn ich in die Qualität des Code-Generators vertraue.
Im folgenden zeige ich exemplarisch einen Unit Test, der die Klasse InMemoryItemStore aus der Persistenz-Schicht testet. Die Klasse selbst beschreibe ich in diesem Blog-Artikel genauer kernkonzepte-von-spring.
const val DEFAULT_ID = "123" internal class InMemoryItemStoreTest {
private lateinit var itemStore : InMemoryItemStore private val idGeneratorMock = Mockito.mock(IdGenerator::class.java) private val firstItem = Item("Schuh", "333")
@BeforeEach fun setUp() { Mockito.`when`(idGeneratorMock.generateId()).thenReturn(DEFAULT_ID) itemStore = InMemoryItemStore(idGeneratorMock) itemStore.items.add(firstItem) }
@Test fun saveItemWithIdNull() { assertEquals(DEFAULT_ID, saveItemWithId(null)) assertEquals(2, itemStore.items.size) }
@Test fun saveItemWithNewId() { assertEquals("567", saveItemWithId("567")) assertEquals(2, itemStore.items.size) }
@Test fun saveItemWithExistingId() { assertEquals(firstItem.id, saveItemWithId(firstItem.id)) assertEquals(1, itemStore.items.size) }
fun saveItemWithId(id: String?) : String { val item = Item("Ball", id) return itemStore.saveItem(item) } ... }
Der hier gezeigte Code ist in der Programmiersprache Kotlin geschrieben. Da Kotlin auf der JVM basiert, kann ich die Java-Frameworks JUnit und Mockito verwenden.
Die 3 hier gezeigten Tests (markiert mit der JUnit @Test Annotation) testen die saveItem Methode der InMemoryItemStore Klasse. Jeder Test durchläuft dabei einen anderen Pfad in der zu testenden Methode.
Um wirklich einen Unit Test zu schreiben, habe ich die IdGenerator Instanz, die im zu testenden InMemoryItemStore Objekt verwendet wird, durch einen Mockito Mock ersetzt. Würde ich das nicht tun, würde dieser Test den Code von 2 Klassen testen und somit die Unit Test Grenze von einer Unit bzw. Klasse verlassen. Weitere Infos zu Mockito als Mocking Framework gibt es hier spring-and-mockito.html.
Ansonsten habe ich bei diesem Test versucht die Clean Code Regeln für Unit Tests so gut wie möglich zu befolgen, deshalb komme ich in jeder Methode auch mit 2-3 Zeilen Code aus. Für weitere Informationen zu Clean Code in JUnit Tests schaut euch diesen Blog-Artikel an JUnit5andSpringBootTest.html.
Integrationstests mit JUnit 5 und Spring Boot
Die mittlere Ebene der hier gezeigten Testpyramide können wir auch noch mit unserem einfachen Technologie-Stack abdecken, indem wir Spring Boot Tests erstellen und in diesen HTTP-Requests gegen die REST-API schicken, die von unseren Spring Rest-Controller bereitgestellt wird.
Der Spring Boot Test kann die ganze Spring Anwendung (oder auch nur Teile davon) starten und somit auch einen HTTP-Endpunkt bereitstellen. Diesen rufen wir mit einem HTTP Client (hier WebTestClient) auf, um den Rest-Controller und die darunter liegende Logik zu testen. Wir durchlaufen also deutlich mehr Code in unterschiedlichen Klassen des Microservice als beim zuvor gezeigten Unit-Test. Hier ist der Integrationstest für die durch den ItemController bereitgestellte REST-API:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) internal class ItemControllerWithWebTestClientTest( @Autowired val store: InMemoryItemStore) {
private val client = WebTestClient.bindToServer() .baseUrl("http://localhost:8080/item").build()
@Test fun getRequest() { client.get().uri("?id=1.5").exchange() .expectStatus().is2xxSuccessful .expectBody() .jsonPath("$.name").isEqualTo("Kotlin") }
@Test fun deleteRequest() { client.delete().uri("/5").exchange() .expectStatus().isNoContent assertEquals(1, store.items.size) }
@Test fun postRequest() { client.post().bodyValue(Item("Java", "11")).exchange() .expectStatus().isCreated assertEquals(3, store.items.size) }
@Test fun getBadRequest() { client.get().uri("?id=5").exchange() .expectStatus().isBadRequest } ... }
Durch die @SpringBootTest mit webEnvironment Attribut werden alle Spring Beans und somit der komplette Spring Microservice instanziiert und mit einem Application-Server (Tomcat oder Netty je nach Maven-Konfiguration) bereitgestellt.
Im Testklassen Attribut client erstelle ich eine Instanz des Spring WebTestClient. Mit dieser kann ich HTTP-Requests gegen den im Spring Boot Test erzeugten Application Server schicken. Der WebTestClient bietet der EntwicklerIn praktische Methoden zum Testen der HTTP-Response vom Server, siehe expectStatus und expectBody im hier gezeigten Test.
Für jede HTTP-Operation habe ich hier mindestens einen positiven Test gezeigt, siehe die Funktionen getRequest, deleteRequest und postRequest. Exemplarisch habe ich auch einen negativen Testfall getBadRequest gezeigt, bei dem der Parameter id=5 an der Validierung scheitert.
Ende zu Ende Tests mit Postman
Für die E2E Tests unsere REST-API verwende ich sehr gerne Postman. Mit Postman kann man Testsuite erstellen, die eine Reihe von verschiedenen HTTP-Requests abschicken, um so die UseCases der Clients unseres Microservices nachzustellen. Für unsere hier gezeigte Item-API könnte ein Portal zum Beispiel folgenden UseCase haben:
Zeige alle Items an (GET /item)
Erzeuge ein neues Item (POST /item mit Item-JSON als Request Body)
Zeige alle Items inklusive dem neu erzeugtem Item (GET /item)
Mit Postman kann ich diese 3 HTTP-Requests der Reihe nach ausführen. Ich kann mit einfachem JavaScript die HTTP-Responses der 3 Requests auswerten und sogar Zwischenergebnisse (z.B. die Id des im 2. POST-Request erzeugten Items) in den nächsten Schritten verwenden.
Test Automatisierung in der CICD Pipeline
Ein Vorteil des hier gezeigten Technologie-Stacks (ohne Postman) ist, dass die JUnit Tests mit unserem Build-Tool (Maven oder Gradle) ausgeführt werden können. Spring Boot unterstützt beim Konfigurieren des hier gezeigten Technologie-Stacks, da Mockito und der WebTestClient standardmäßig in jedem neu generierten Spring Boot Projekt als Bibliothek verfügbar sind. Deshalb können wir einfach den Build starten, um alle Tests automatisch auszuführen.
Unsere CICD Pipeline braucht in jedem Fall ein Build-Tool zum Bauen unseres Services. Das ist in jeder CICD Pipeline einer der ersten Schritte. Wenn wir das haben, ist es auch sehr leicht die JUnit-Tests mit jeder Pipeline-Ausführung zu starten. In der standardmäßigen Startkonfiguration führt z.B. Maven alle JUnit-Tests automatisch aus. Aus meiner Sicht ist es eine gute Idee mit jedem Commit ins Sourcecode-Repository die CICD-Pipeline automatisch zu starten und mindestens bis nach dem Schritt "JUnit Tests ausführen" laufen zu lassen. Auf diese Weise bekommt jeder Entwickler direkt Feedback, ob sein Commit (aus Versehen) einen der Unit oder Integrationstests fehlschlagen lässt - das sollte der Entwickler dann so schnell wie möglich analysieren und korrigieren, um andere Entwicklerinnen nicht zu stören.
Ende zu Ende Tests mit Postman können ebenfalls automatisch durch die CICD Pipeline ausgeführt werden. Das ist aber etwas komplizierter einzurichten als die JUnit Test Ausführung mit Maven. Hier ist es z.B. erklärt: https://dev.to/.../hello-newman
Fazit
Die Test-Pyramide gilt auch für Microservices oder andere Services mit REST-API. Es gibt hier viele verschiedene Tools zur Testautomatisierung. Ich habe hier die Standard-Tools vorgestellt, weil sie einen Großteil der Pyramide abdecken können und leicht in eine automatisierte CICD Pipeline integriert werden können. Ein weiterer Vorteil ist, dass die meisten EntwicklerInnen diese Tools kennen oder leicht lernen können.
Damit gehören Code und ein Großteil der Testautomatisierung zusammen und wir müssen sicherstellen, dass Code und Tests wartbar sind. Ich habe beobachtet, dass sich Entwickler beim schreiben wartbarer JUnit-Tests schwerer tun als beim Schreiben von sauberem Code. Daher ist es extrem wichtig, dass ihr auch bei den JUnit-Tests die Clean Code Regeln zum Schreiben von sauberem und wartbarem Code befolgt.
Solltet ihr euch dafür interessieren, wie der Microservice in Kotlin geschrieben wurde, empfehle ich euch dazu meinen Udemy Kurs: https://www.udemy.com/course/api-und-kotlin
Authentifizierung mit OpenID Connect geht einfach dank Spring Boot. Wir bauen den Login Deiner Web-Anwendung mit dem Autorisierungsserver Deiner Firma. Wie wir dazu OpenID Connect mit Spring Boot 3 konfigurieren, zeige ich in diesem Blog-Artikel. OpenID Connect - Authorization Code Prozess OpenID Connect ist ein Single Sign-On Login-Verfahren. Zur Umsetzung komplexer Anwendungsfälle verwenden größere Firmen meist verteilte Systeme. Damit der Benutzer z. B. beim Online-Shopping den Systemwechsel von Produktseiten zum Einkaufswagen und zur Kasse nicht wahrnimmt, loggt er sich mittels Single Sign-On nur einmal ein. Die beteiligten Systeme authentifizieren den eingeloggten Benutzer anhand seiner Single Sign-On Session. Detaillierte Informationen über OpenID Connect und Single Sign-On findet ihr hier . In diesem Artikel fokussiere ich mich auf das Anwendungs-System, welches zur Benutzer-Authentifizierung den firmeneigenen OpenID Identity Provider verwendet. Bevor wir uns die Implementierung
Mit Spring können zeitgesteuerte Aufgaben in Java Code integriert werden. CronJobs wie wir sie in Linux kennen, definieren wir mit Spring einfach per Annotation. In diesem Artikel zeige ich wie das geht und wie Spring die CronJobs entsprechend unserer Definition ausführt. Was ist ein CronJob? Unter CronJob verstehen wir die zeitlich gesteuerte Ausführung eines Kommandos zur Erledigung einer Aufgabe bzw. eines Jobs. Das Kommando wird durch einen bestimmten Zeitpunkt oder eine zeitliche Bedingung angestoßen. Typische Beispiele für durch CronJobs gestartete Aufgaben sind: Regelmäßiges Aufräumen der Datenbank - z. B. um veraltete Daten zu löschen oder DSGVO konform persönliche Daten nach einer definierten Zeit zu löschen. Wöchentlicher Versand von Newslettern oder Werbung per Email Nächtliche Datenbank-Backups Monatliches Erstellen von Rechnungen (z.B. Telefon-Rechnung) Das Betriebssystem Linux bietet crontab zum Erstellen von CronJobs an. Mit crontab könnten wir eine Spring Anwendung star
In diesem Blog-Post besprechen wir die Basics von Spring: Spring Beans und wie man diese miteinander vernetzt bzw. referenziert (Dependency Injection). Den kompletten Code zu diesem Blog-Post findet ihr in GitHub: https://github.com/elmar-brauch/beans Spring Bean Spring Beans sind Java Objekte, die durch den Spring IoC Container instanziiert und verwaltet werden. Der IoC (Inversion of Control) Container erstellt Beans anhand einer Bean Definition, die der Entwickler in Form von Annotationen oder xml Konfiguration bereitstellt. IoC ist ein Umsetzungsparadigma und bedeutet Steuerungsumkehr. Im Kontext des Spring Frameworks versteht man darunter, dass das Framework die Erstellung des Objektnetzes (Beans) anstelle des Entwicklers übernimmt. In vorherigen Blog-Artikel haben wir bereits Projekte mit Spring Boot aufgesetzt, siehe z.B. microservices-mit-spring-boot-erstellen.html . In diesen Projekten verwendeten wir die Annotation @SpringBootApplication , welche eine Aggregation diverser
Kommentare