Das Spring Framework wird häufig als Beispiel für das Cloud Native Framework angeführt, das für die Arbeit in der Cloud, die Entwicklung von Zwölf-Faktor-Anwendungen , Microservices und eines der stabilsten, aber gleichzeitig innovativsten Produkte entwickelt wurde. Aber in diesem Artikel möchte ich auf eine weitere starke Seite des Frühlings eingehen: Es ist seine Entwicklungsunterstützung durch Testen (TDD-Fähigkeit?). Trotz TDD-Konnektivität habe ich oft bemerkt, dass Spring-Projekte entweder einige bewährte Methoden zum Testen ignorieren, ihre eigenen Motorräder erfinden oder überhaupt keine Tests schreiben, weil sie "langsam" oder "unzuverlässig" sind. Und ich werde Ihnen genau sagen, wie Sie schnelle und zuverlässige Tests für Anwendungen auf dem Spring Framework schreiben und die Entwicklung durch Testen durchführen. Wenn Sie also Spring verwenden (oder starten möchten), verstehen, was Tests im Allgemeinen sind (oder verstehen möchten) oder denken, dass contextLoads
die notwendige und ausreichende Stufe für Integrationstests ist - das wird interessant sein!
Die "TDD" -Funktion ist sehr vieldeutig und schlecht messbar. Dennoch hat Spring viele Dinge, die von Natur aus dazu beitragen, Integrations- und Komponententests mit minimalem Aufwand zu schreiben. Zum Beispiel:
- Integrationstests - Sie können die Anwendung einfach starten, Komponenten sperren, Parameter neu definieren usw.
- Fokusintegrationstests - nur Zugriff auf Daten, nur Web usw.
- Sofort einsatzbereite Unterstützung - speicherinterne Datenbanken, Nachrichtenwarteschlangen, Authentifizierung und Autorisierung in Tests
- Testen durch Verträge (Spring Cloud Contract)
- Unterstützung für das Testen der Web-Benutzeroberfläche mit HtmlUnit
- Flexibilität der Anwendungskonfiguration - Profile, Testkonfigurationen, Komponenten usw.
- Und vieles mehr
Zunächst eine kleine, aber notwendige Einführung in TDD und das Testen im Allgemeinen.
Testgetriebene Entwicklung
TDD basiert auf einer sehr einfachen Idee: Wir schreiben Tests, bevor wir Code schreiben. Theoretisch klingt es beängstigend, aber nach einiger Zeit kommt ein Verständnis für Praktiken und Techniken, und die Möglichkeit, Tests danach zu schreiben, verursacht spürbare Beschwerden. Eine der Schlüsselpraktiken ist die Iteration , d.h. Machen Sie alles zu kleinen, fokussierten Iterationen, von denen jede als Rot-Grün-Refaktor bezeichnet wird .
In der roten Phase schreiben wir einen Falltest, und es ist sehr wichtig, dass er mit einem klaren, verständlichen Grund und einer klaren Beschreibung fällt und dass der Test selbst vollständig ist und bestanden wird, wenn der Code geschrieben wird. Der Test sollte das Verhalten überprüfen, nicht die Implementierung , d. H. Folgen Sie dem Black-Box-Ansatz, dann werde ich erklären, warum.
In der grünen Phase schreiben wir den minimal erforderlichen Code , um den Test zu bestehen. Manchmal ist es interessant zu üben und es so verrückt wie möglich zu machen (obwohl es besser ist, sich nicht mitreißen zu lassen), und wenn eine Funktion je nach Zustand des Systems einen Booleschen Wert zurückgibt, kann der erste "Durchgang" einfach return true
.
In der Refactoring- Phase, die nur gestartet werden kann, wenn alle Tests grün sind , werden wir den Code refactorisieren und in den richtigen Zustand bringen. Es ist nicht einmal für einen Code erforderlich, den wir geschrieben haben. Daher ist es wichtig, mit dem Refactoring auf einem stabilen System zu beginnen. Der „Black Box“ -Ansatz hilft nur beim Refactoring, beim Ändern der Implementierung, aber nicht beim Berühren des Verhaltens.
Ich werde in Zukunft über verschiedene Aspekte von TDD sprechen, schließlich ist dies die Idee einer Reihe von Artikeln, daher werde ich mich jetzt nicht besonders mit den Details befassen. Bevor ich jedoch auf Standard-TDD-Kritik reagiere, werde ich einige Mythen erwähnen, die ich oft höre.
- "TDD deckt den Code zu 100% ab, gibt jedoch keine Garantie" - die Entwicklung durch Tests hat überhaupt keinen Bezug zur 100% igen Abdeckung. In vielen Teams, in denen ich gearbeitet habe, wurde diese Metrik nicht einmal gemessen und als Eitelkeitsmetrik klassifiziert. Und ja, 100% Testabdeckung bedeutet nichts.
- "TDD funktioniert nur für einfache Funktionen, eine echte Anwendung mit einer Datenbank und ein schwieriger Zustand kann damit nicht erstellt werden" ist eine sehr beliebte Ausrede, die normalerweise ergänzt wird durch "Wir haben eine so komplizierte Anwendung, dass wir überhaupt keine Tests schreiben, Sie können es überhaupt nicht tun." Ich sah einen funktionierenden TDD-Ansatz für völlig unterschiedliche Anwendungen - Web (mit und ohne SPA), Mobile, API, Microservices, Monolithen, komplexe Bankensysteme, Cloud-Plattformen, Frameworks, Einzelhandelsplattformen, die in verschiedenen Sprachen und Technologien geschrieben wurden. Der populäre Mythos „Wir sind einzigartig, alles ist anders“ ist meistens eine Ausrede, keinen Aufwand und kein Geld in Tests zu investieren, aber kein wirklicher Grund (obwohl es auch echte Gründe geben kann).
- "Es wird immer noch Fehler mit TDD geben" - natürlich wie bei jeder anderen Software. Bei TDD geht es nicht um Fehler oder deren Abwesenheit, sondern um ein Entwicklungswerkzeug. Wie das Debuggen. Wie eine IDE. Wie die Dokumentation. Keines dieser Tools garantiert das Fehlen von Fehlern, sondern hilft nur, die zunehmende Komplexität des Systems zu bewältigen.
Das Hauptziel von TDD und allgemeinen Tests besteht darin, dem Team das Vertrauen zu geben , dass das System stabil funktioniert. Daher bestimmt keine der Testpraktiken, wie viele und welche Tests geschrieben werden sollen. Schreiben Sie, wie viel Sie für notwendig halten, wie viel Sie benötigen, um sicherzugehen, dass der Code jetzt in Produktion gehen kann und funktioniert . Es gibt Leute, die schnelle Integrationstests als eine ultimative Black Box betrachten, die notwendig und ausreichend ist, und Unit-Tests optional. Jemand sagt, dass e2e-Tests mit der Möglichkeit eines schnellen Rollbacks auf die vorherige Version und dem Vorhandensein kanarischer Versionen nicht so kritisch sind. Wie viele Teams - so viele Ansätze, es ist wichtig, eigene zu finden.
Eines meiner Ziele ist es, mich vom Format „Entwicklung durch Testen einer Funktion, die zwei Zahlen hinzufügt“ in der TDD-Story zu entfernen und eine reale Anwendung zu betrachten, eine Art Testpraxis, die auf eine minimale Anwendung reduziert wurde und in realen Projekten gesammelt wurde. Als solches semi-reales Beispiel werde ich eine kleine Webanwendung verwenden, die ich selbst für abstrakt erfunden habe Fabriken Bäckerei - Kuchenfabrik . Ich habe vor, kleine Artikel zu schreiben, die sich jedes Mal auf eine separate Anwendungsfunktionalität konzentrieren und durch TDD zeigen, dass Sie APIs und die interne Struktur der Anwendung entwerfen und ein konstantes Refactoring beibehalten können.
Ein Beispielplan für eine Reihe von Artikeln, wie ich ihn derzeit sehe, lautet:
- Laufskelett - Anwendungsframework, in dem Sie den Rot-Grün-Refaktor-Zyklus ausführen können
- UI-Tests und verhaltensgesteuertes Design
- Datenzugriffstests (Federdaten)
- Autorisierungs- und Authentifizierungstests (Spring Security)
- Jet Stack (WebFlux + Projektreaktor)
- Interoperabilität von (Mikro-) Diensten und Verträgen (Spring Cloud)
- Testen der Nachrichtenwarteschlange (Spring Cloud)
In diesem Einführungsartikel geht es um die Punkte 1 und 2 - Ich werde ein Anwendungsframework und einen grundlegenden UI-Test unter Verwendung des BDD- oder verhaltensgesteuerten Entwicklungsansatzes erstellen. Jeder Artikel beginnt mit einer User Story , aber ich werde nicht über den Teil „Produkt“ sprechen, um Zeit zu sparen. Die User Story wird in englischer Sprache verfasst, es wird bald klar, warum. Alle Codebeispiele finden Sie auf GitHub, daher werde ich nicht den gesamten Code analysieren, sondern nur die wichtigen Teile.
User Story ist eine Beschreibung einer Funktion einer Anwendung in natürlicher Sprache, die normalerweise im Auftrag eines Benutzers des Systems geschrieben wird.
User Story 1: Benutzer sieht Begrüßungsseite
Als Alice ein neuer Benutzer
Ich möchte eine Begrüßungsseite sehen, wenn ich die Cake Factory-Website besuche
Damit ich weiß, wann Cake Factory startet
Akzeptanzkriterien:
Szenario: Ein Benutzer, der die Website besucht, besucht sie vor dem Startdatum
Vorausgesetzt, ich bin ein neuer Benutzer
Wenn ich die Cake Factory-Website besuche
Dann sehe ich eine Nachricht 'Danke für Ihr Interesse'
Und ich sehe eine Nachricht 'Die Website kommt bald ...'
Es wird Wissen erfordern: Was ist verhaltensgesteuerte Entwicklung und Gurke , die Grundlagen des Spring Boot-Testens .
Die erste User Story ist recht einfach, aber das Ziel liegt noch nicht in der Komplexität, sondern in der Erstellung eines Laufskeletts - eine minimale Anwendung zum Starten des TDD-Zyklus .
Nachdem ich ein neues Projekt auf Spring Initializr mit Web- und Moustache-Modulen erstellt habe, benötige ich zunächst einige weitere Änderungen an build.gradle
:
- Fügen Sie
testImplementation('net.sourceforge.htmlunit:htmlunit')
testImplementation testImplementation('net.sourceforge.htmlunit:htmlunit')
. Sie müssen die Version nicht angeben. Das Spring Boot-Abhängigkeitsverwaltungs-Plugin für Gradle wählt automatisch die erforderliche und kompatible Version aus - ein Projekt von JUnit 4 auf JUnit 5 migrieren (da 2018 auf dem Hof liegt)
- Fügen Sie Cucumber Abhängigkeiten hinzu - eine Bibliothek, mit der ich BDD-Spezifikationen schreiben werde
- Entfernen Sie standardmäßig erstellte
CakeFactoryApplicationTests
mit unvermeidlichen contextLoads
Im Großen und Ganzen ist dies das grundlegende "Grundgerüst" der Anwendung, Sie können bereits den ersten Test schreiben.
Um die Navigation im Code zu vereinfachen, werde ich kurz auf die verwendeten Technologien eingehen.
Gurke
Cucumber ist ein verhaltensgesteuertes Entwicklungsframework , mit dessen Hilfe "ausführbare Spezifikationen" erstellt werden können, d. H. Führen Sie Tests (Spezifikationen) in natürlicher Sprache durch. Das Cucumber-Plugin analysiert den Quellcode in Java (und vielen anderen Sprachen) und verwendet Schrittdefinitionen , um echten Code auszuführen. @Given
sind @When
, die mit @Given
, @When
, @Then
und anderen Annotationen versehen sind.
Htmlunit
Die Projekthomepage nennt HtmlUnit "einen GUI-freien Browser für Java-Anwendungen". Im Gegensatz zu Selenium startet HtmlUnit keinen echten Browser und rendert die Seite vor allem überhaupt nicht, da es direkt mit dem DOM arbeitet. JavaScript wird von der Mozilla Rhino Engine unterstützt. HtmlUnit eignet sich gut für klassische Anwendungen, ist jedoch für Single Page Apps nicht sehr geeignet. Zunächst wird es ausreichen, und dann werde ich versuchen zu zeigen, dass auch solche Dinge wie ein Testframework Teil der Implementierung und nicht die Grundlage der Anwendung sein können.
Erster Test
Jetzt wird mir eine auf Englisch geschriebene User Story nützlich sein. Der beste Auslöser für den Start der nächsten TDD-Iteration sind Akzeptanzkriterien, die so geschrieben sind, dass sie mit einem Minimum an Gesten in eine ausführbare Spezifikation umgewandelt werden können.
Im Idealfall sollten User Stories so geschrieben werden, dass sie einfach in die BDD-Spezifikation kopiert und ausgeführt werden können. Dies ist alles andere als immer einfach und nicht immer möglich, aber dies sollte das Ziel des Produktbesitzers und des gesamten Teams sein, wenn auch nicht immer erreichbar.
Also mein erstes Feature.
Feature: Welcome page Scenario: a user visiting the web-site visit before the launch date Given a new user, Alice When she visits Cake Factory web-site Then she sees a message 'Thank you for your interest' And she sees a message 'The web-site is coming in December!'
Wenn Sie Schrittbeschreibungen erstellen (das Intellij IDEA-Plugin unterstützt Gherkin sehr) und den Test ausführen, ist er natürlich grün - es wird noch nichts getestet. Und hier kommt die wichtige Phase der Arbeit am Test - Sie müssen einen Test schreiben, als ob der Hauptcode geschrieben worden wäre .
Für diejenigen, die anfangen, TDD zu verbannen, setzt hier oft eine Betäubung ein - es ist schwierig, die Algorithmen und die Logik von etwas, das nicht existiert, in den Kopf zu bekommen. Daher ist es sehr wichtig, möglichst kleine und fokussierte Iterationen zu haben, angefangen von der User Story bis hin zur Integrations- und Einheitenebene. Es ist wichtig, sich auf jeweils einen Test zu konzentrieren und zu versuchen, nass zu werden und Abhängigkeiten zu ignorieren, die noch nicht wichtig sind. Manchmal ist mir aufgefallen, wie leicht Leute beiseite gehen - eine Schnittstelle oder Klasse für eine Abhängigkeit erstellen, sofort eine leere Testklasse dafür generieren, dort eine weitere Abhängigkeit hinzufügen, eine weitere Schnittstelle erstellen und so weiter.
Wenn die Geschichte lautet "Es wäre notwendig, den Status beim Speichern zu aktualisieren", ist es sehr schwierig, sie zu automatisieren und zu formalisieren. In meinem Beispiel kann jeder Schritt in einer Abfolge von Schritten klar angeordnet werden, die durch Code beschrieben werden können. Es ist klar, dass dies das einfachste Beispiel ist und nicht viel zeigt, aber ich hoffe, dass es mit zunehmender Komplexität interessanter wird.
Rot
Daher habe ich für mein erstes Feature die folgenden Schrittbeschreibungen erstellt:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WelcomePage { private WebClient webClient; private HtmlPage page; @LocalServerPort private int port; private String baseUrl; @Before public void setUp() { webClient = new WebClient(); baseUrl = "http://localhost:" + port; } @Given("a new user, Alice") public void aNewUser() {
Einige Punkte, die Sie beachten sollten:
- Features werden von einer anderen Datei,
Features.java
mit RunWith
Annotation von JUnit 4 gestartet. Cucumber unterstützt leider keine Version 5 @SpringBootTest
Annotation @SpringBootTest
wird zur Beschreibung der Schritte hinzugefügt, cucumber-spring
nimmt sie von dort auf und konfiguriert den @SpringBootTest
(d. H. Startet die Anwendung).- Die Spring-Anwendung für den Test beginnt mit
webEnvironment = RANDOM_PORT
und dieser zufällige Port wird mit @LocalServerPort
an den Test @LocalServerPort
. Spring findet diese Anmerkung und setzt den @LocalServerPort
auf den Server-Port
Und der Test stürzt erwartungsgemäß mit dem Fehler 404 for http://localhost:51517
.
Die Fehler, mit denen der Test abstürzt, sind unglaublich wichtig, insbesondere bei Unit- oder Integrationstests. Diese Fehler sind Teil der API. Wenn der Test mit einer NullPointerException
dies nicht allzu gut, aber die BaseUrl configuration property is not set
- viel besser.
Grün
Um den Test grün zu machen, habe ich einen Basis-Controller und eine Ansicht mit minimalem HTML-Code hinzugefügt:
@Controller public class IndexController { @GetMapping public String index() { return "index"; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cake Factory</title> </head> <body> <h1>Thank you for your interest</h1> <h2>The web-site is coming in December!</h2> </body> </html>
Der Test ist grün, die Anwendung funktioniert, obwohl er in der Tradition des strengen Konstruktionsdesigns hergestellt wird.
Bei einem echten Projekt und in einem ausgeglichenen Team würde ich mich natürlich mit dem Designer zusammensetzen und wir würden das bloße HTML in etwas viel Schöneres verwandeln. Aber im Rahmen des Artikels wird kein Wunder geschehen, die Prinzessin wird ein Frosch bleiben.
Die Frage „Was für ein Teil von TDD ist Design?“ Ist nicht so einfach. Eine der Methoden, die ich als nützlich empfunden habe, besteht darin, zunächst gar nicht auf die Benutzeroberfläche zu schauen (nicht einmal die Anwendung auszuführen, um Ihre Nerven zu schonen), einen Test zu schreiben, ihn grün zu machen - und dann mit einer stabilen Grundlage am Front-End zu arbeiten und die Tests ständig neu zu starten .
Refactor
In der ersten Iteration gibt es kein bestimmtes Refactoring, aber obwohl ich die letzten 10 Minuten damit verbracht habe, eine Vorlage für Bulma auszuwählen , die als Refactoring gezählt werden kann!
Abschließend
Während die Anwendung weder über Sicherheitsarbeit noch über eine Datenbank oder eine API verfügt, sehen Tests und TDDs recht einfach aus. Und im Allgemeinen habe ich von der Testpyramide nur die Spitze berührt, den UI-Test. Aber zum Teil besteht das Geheimnis des Lean-Ansatzes darin, alles in kleinen Iterationen zu erledigen, eine Komponente nach der anderen. Dies hilft, sich auf die Tests zu konzentrieren, sie zu vereinfachen und die Qualität des Codes zu kontrollieren. Ich hoffe, dass es in den folgenden Artikeln interessanter wird.
Referenzen
PS Der Titel des Artikels ist nicht so verrückt, wie es am Anfang scheinen mag, ich denke, viele haben es bereits erraten. "Wie man eine Pyramide in den Kofferraum baut" bezieht sich auf die Testpyramide (dazu später mehr) und Spring Boot, wobei "Boot" im britischen Englisch auch "Kofferraum" bedeutet.