In der Softwareentwicklung sind automatisierte Tests unerlässlich, um fortwährend sicherzustellen, dass Anwendungen zuverlässig und fehlerfrei funktionieren. Sie bieten sowohl den Endnutzenden als auch den Entwicklungsteams Sicherheit und Vertrauen in die Softwarequalität. Gerade die Testing Library von Kent C. Dodds hat sich in den letzten Jahren als ein mächtiges Werkzeug etabliert, das weit über klassische Unit-Tests hinausgeht. Sie fokussiert sich dabei auf Tests für Applikationen, die auf Web-Technologien basieren, wie React, Angular, Vue oder auch React Native.
In diesem Beitrag beleuchten unsere Xperten, warum Tests so wichtig sind, welche Prinzipien hinter der Testing Library stehen und wie sie helfen, die Qualität von Software zu gewährleisten. Automatisierte Tests stellen bei uns im Build-Prozess sicher, dass eine Anwendung unseren hohen Qualitätsstandards entspricht, bevor sie für die Endnutzenden freigegeben wird.
Warum testen?
Tests sind ein integraler Bestandteil erfolgreicher Softwareentwicklung und erfüllen viele wichtige Funktionen:
- Funktionalitäten sicherstellen: Tests gewährleisten, dass eine Anwendung so funktioniert, wie sie gedacht ist. Wenn sich die Anwendung unerwartet verhält, ist es wichtig, dass Tests fehlschlagen.
- Dokumentation: Tests dokumentieren das Verhalten der Anwendung. Sie sind eine lebende Dokumentation, die beschreibt, wie sich die Anwendung in bestimmten Szenarien verhält.
- Automatisierung: Automatisierte Tests verhindern, dass Entwickelnde wichtige Schritte im Testprozess vergessen.
Tests erfüllen also zwei zentrale Aufgaben:
- Sie stellen für die Endnutzenden sicher, dass die Software zuverlässig funktioniert.
- Sie helfen den Entwicklungsteams, keine wichtigen Details zu übersehen, und sparen gleichzeitig wertvolle Zeit. Darüber hinaus stärken Tests das Vertrauen der Entwickelnden in ihre Arbeit. Sie erlauben ihnen eine bessere Bewertung, ob die geforderte Qualität auch erreicht wird.
Vertrauen durch Tests gewinnen
Tests können Vertrauen jedoch nur schaffen, wenn diese aussagekräftig und realitätsnah sind. Eine der größten Herausforderungen bei der Erstellung von Tests ist sicherzustellen, dass sie nicht nur isolierte Einheiten überprüfen, sondern auch die tatsächliche Nutzung der Anwendung widerspiegeln.
Unzureichende Tests lassen sich oft daran erkennen, dass bspw. alle Importe simuliert werden (Mocking), mit Daten gearbeitet wird, die in der realen Anwendung nicht vorkommen oder flache Renderings (Shallow-Rendering) durchgeführt werden, bei denen Child-Components ebenfalls simuliert sind. Je integrativer und realistischer Tests sich verhalten, desto aussagekräftiger sind sie.
Wenn Tests aussagekräftiger sind, steigt die Wahrscheinlichkeit, dass die Tests in einem Fehlerfall auch wirklich fehlschlagen. Dadurch gewinnt das Entwicklungsteam Vertrauen sowohl in die Tests als auch in die Anwendung.
Zusammenfassend lässt sich sagen, um Vertrauen in Tests zu stärken, ist es entscheidend, dass diese nicht nur isolierte Funktionalitäten überprüfen, sondern möglichst nah an der realen Nutzung durch die Endnutzenden orientiert sind. Hier setzt die Testing Library an, die speziell entwickelt wurde, um diese Lücke zu schließen. Sie hilft Entwicklungsteams dabei, ihre Tests praxisnäher – mit den Nutzenden im Fokus – zu gestalten und somit die Aussagekraft der Tests sowie das Vertrauen in die gesamte Anwendung zu erhöhen.
Was ist die Testing Library und woher kommt sie?
Die Testing Library ist eine Sammlung von Testing Utilities, die von Kent C. Dodds, einem JavaScript-Entwickler und Lehrer, ins Leben gerufen wurde. Sie wurde entwickelt, um die Art und Weise zu verbessern, wie Entwickelnde grafische Oberflächen (UIs) testen. Die Testing Library verfolgt das Prinzip: Je näher deine Tests der tatsächlichen Nutzung deiner Software kommen, desto mehr Vertrauen können sie dir geben. Dieses Prinzip betont die Wichtigkeit, Tests so zu gestalten, dass sie das tatsächliche User-Verhalten möglichst realistisch abbilden, um die Aussagekraft der Tests und das Vertrauen in die Software zu erhöhen.
Kent C. Dodds entwickelte die Testing Library als Reaktion auf traditionelle Testing-Methoden, die oft zu stark auf interne Implementierungsdetails fokussieren und weniger auf die reale User-Erfahrung. Er wollte ein Werkzeug kreieren, das Tests stärker an den tatsächlichen Interaktionen ausrichtet, die Endnutzende mit der Software haben.
Die Prinzipien der Testing Library
- Realitätsnahe Tests: Das Hauptziel der Testing Library ist es, Tests so nah wie möglich an der realen Nutzung der Software zu gestalten. Dadurch wird die Aussagekraft der Tests erhöht und es entsteht mehr Vertrauen in die Ergebnisse.
- Framework-Unabhängigkeit: Die Testing Library ist nicht auf ein spezifisches JavaScript-Framework beschränkt. Sie bietet Implementierungen für verschiedene Frameworks wie React (React Testing Library), Angular (Angular Testing Library), Vue (Vue Testing Library) und auch für generisches DOM-Testing, wie bspw. für Cypress (Cypress Testing Library). Das macht sie vielseitig und breit einsetzbar – sie ist somit Framework-agnostisch.
- Fokus auf Barrierefreiheit (Accessibility): Ein zentrales Konzept der Testing Library ist die Priorisierung von barrierefreien Abfragen (Accessibility Queries). Diese Queries simulieren die Interaktionen, die Nutzende mit einem Screenreader oder einer anderen Assistenztechnologie durchführen würden. So wird sichergestellt, dass die Kernstruktur der Anwendung auch für User mit Einschränkungen gut nutzbar ist.
- Minimale Kenntnis interner Details: Die Testing Library ermutigt dazu, Tests zu schreiben, die wenig bis keine Kenntnis über die internen Details der Komponenten haben. Stattdessen sollte der Fokus auf den sichtbaren und zugänglichen Teilen der UI liegen. Das fördert eine bessere Wartbarkeit der Tests und verhindert, dass Tests durch Änderungen in der Implementierung fehlschlagen, die aus User-Sicht irrelevant sind.
- Nutzungszentrierte Interaktionen: Anstatt spezifische DOM-Ereignisse direkt auszulösen (z. B.
fireEvent.click
), ermutigt die Testing Library zur Verwendung vonuserEvent
, das realistischere User-Interaktionen simuliert. Dadurch sind die Tests robuster und näher an den tatsächlichen Erfahrungen der Nutzenden.
Diese Prinzipien spiegeln sich auch in der von Kent C. Dodds entwickelten Testing Trophy wider, einer modernen Alternative zur klassischen Testing-Pyramide. Die Testing Trophy konzentriert sich auf den Return on Investment (ROI) der Tests und betont dabei die Wichtigkeit von integrativen Tests.
Testing Trophy
- Statische Tests: Tools wie ESLint, Typed Languages oder Sonar analysieren den Code ohne Ausführung.
- Unit- und Komponententests: Diese Tests überprüfen einzelne Funktionen oder Komponenten isoliert.
- Service- und Integrationstests: Tests, die die Interaktion mehrerer Komponenten oder Systeme überprüfen.
- End-to-End (E2E) Tests: Diese Tests simulieren den kompletten Ablauf aus der Perspektive der Endnutzenden.
Wichtige Tools und Konzepte der Testing Library
- Queries: Die Testing Library stellt verschiedene Abfragemethoden bereit, wie
getByRole
,getByLabelText
odergetByText
. Diese Methoden werden nach ihrer Priorität geordnet, wobei die Abfragen, die am besten mit der tatsächlichen Nutzung der Anwendung übereinstimmen (z. B.getByRole
), bevorzugt werden. - userEvent vs. fireEvent:
userEvent
simuliert komplexere Interaktionen, die dem tatsächlichen User-Verhalten entsprechen, währendfireEvent
direkt DOM-Ereignisse auslöst.userEvent
wird bevorzugt, da es realistischere Szenarien abdeckt. - Jest-DOM: Hierbei handelt es sich um eine Erweiterung der Testing Library, die zusätzliche Matcher für Jest (ein JavaScript-Testing-Framework) bietet. Sie ermöglicht einfache Überprüfungen des DOM-Zustands, wie
toBeVisible
,toHaveTextContent
und andere.
Beispiel
Um die Anwendung der Testing Library greifbarer zu machen, haben unsere Xperten ein kompaktes Beispiel entwickelt. Dabei kommen alle zuvor genannten Tools und Konzepte zum Einsatz: Getestet wird eine Komponente mit interaktiven Elementen. Das Beispiel ist in Angular umgesetzt, lässt sich aber auch problemlos auf andere Frameworks übertragen.
Die Komponente
Wir starten mit einem Template für unsere Beispielkomponente. Es enthält verschiedene Überschriften sowie einen „Tipp des Tages“, der per Button ausgeblendet werden kann.
<div>
<h1>Hello Blog</h1>
@if (isSectionVisible()) {
<section>
<h2>Tipp des Tages</h2>
<p>Testing Library macht Refactorings sicherer.</p>
</section>
} @else {
<section>
<h2>Du hast den Tipp des Tages schon gesehen.</h2>
</section>
}
<button type="button" [disabled]="!isSectionVisible()" (click)="hideTipOfTheDay()">Ausblenden</button>
</div>
Die Logik liegt wie gewohnt im TypeScript der Komponente. Hier haben wir eine Aktion implementiert, die durch einen Button-Klick ausgelöst wird, sowie ein Signal, das das Template reaktiv gestaltet. Signale haben wir bereits in unserem Blog vorgestellt.
import { Component, signal } from "@angular/core";
@Component({
selector: "hello-blog",
standalone: true,
imports: [],
templateUrl: "./hello-blog.component.html",
})
export class HelloBlogComponent {
public isSectionVisible = signal<boolean>(true);
public hideTipOfTheDay(): void {
this.isSectionVisible.set(false);
}
}
Die Tests
Im Rahmen des Tests wollen wir sicherstellen, dass zunächst der initiale Zustand unseren Erwartungen entspricht. In einem zusätzlichen Test möchten wir absichern, dass der Tipp des Tages korrekt ausgeblendet wird, sollte ein User auf den Button zum Ausblenden klicken.
Setup
Wir beginnen zunächst mit dem Setup, damit wir uns im Anschluss auf die eigentlichen Tests konzentrieren können. Innerhalb des Setups initialisieren wir userEvent
, um Interaktionen durch einen potenziellen User innerhalb der Tests auszulösen. Danach erfolgt das Rendern der Komponente über die Render-Methode der Testing Library. An dieser Stelle können grundsätzlich auch Inputs oder die Test-Umgebung selbst konfiguriert werden. Das Nutzen einer Setup-Methode gleicht dem empfohlenen Setup, wenn man Tests mit userEvent
schreiben will (Link).
import { HelloBlogComponent } from "src/hello-blog/hello-blog.component";
import { render, screen } from "@testing-library/angular";
import { userEvent } from "@testing-library/user-event";
const setup = async () => {
const user = userEvent.setup();
const view = await render(HelloBlogComponent);
return { user, view };
};
describe("MyComponentComponent", () => {
…
});
Test des initialen Zustands
Mithilfe unserer Setup-Methode können wir nun unsere Tests schreiben. Wir beginnen mit dem Test, der den initialen Zustand sicherstellen soll. Durch das Setup stoßen wir ein Rendering an und warten ab, bis dieses durchgeführt worden ist. Danach fragen wir auf dem Screen – einem globalen Objekt der Testing Library – und den dahinter liegenden Queries die einzelnen Elemente in unserer Testumgebung ab. Da wir über die Definition im Template viele Rollen durch entsprechende HTML-Tags vergeben haben, können wir diese wie folgt nutzen.
describe("MyComponentComponent", () => {
it("should render initially with heading and tip of the day ", async () => {
// GIVEN
await setup();
// THEN
expect(screen.getByRole("heading", { level: 1, name: "Hello Blog" })).toBeVisible();
expect(screen.getByRole("heading", { level: 2, name: "Tipp des Tages" })).toBeVisible();
…
});
});
So können wir bspw. das <h1>
-Element über die Query
getByRole("heading", { level: 1, name: "Hello Blog" })
erhalten. Hierbei lässt sich erkennen, dass der Textinhalt der Überschrift über die Option name
im gleichen Zuge mit abgefragt werden kann und somit unsere Query noch genauer macht.
Zusätzlich können wir Elemente direkt über ihren Textinhalt abfragen. Im folgenden Testabschnitt zeigen wir, wie sich die Sichtbarkeit eines Elements prüfen lässt: Dafür verwenden wir die getBy
-Query zusammen mit toBeVisible
, um sicherzustellen, dass ein Element sichtbar ist. Möchten wir hingegen testen, dass ein Element nicht sichtbar ist, nutzen wir die queryBy
-Query in Kombination mit not.toBeInTheDocument
.
describe("MyComponentComponent", () => {
it("should render initially with heading and tip of the day", async () => {
…
expect(screen.getByText("Testing Library macht Refactorings sicherer.")).toBeVisible();
expect(
screen.queryByRole("heading", { level: 2, name: "Du hast den Tipp des Tages schon gesehen." })
).not.toBeInTheDocument();
…
});
});
Im letzten Abschnitt unseres ersten Tests nutzen wir den Jest-DOM-Matcher toBeDisabled
, um sicherzustellen, dass der Button zum Ausblenden der Inhalte zu Beginn aktiv und anklickbar ist. Ein Vorteil dieser Matcher ist, dass sie uns ermöglichen, auch die richtigen ARIA-Attribute zu überprüfen – so stellen wir sicher, dass zumindest die Grundanforderungen an die Barrierefreiheit erfüllt sind und Nutzende, die auf Screenreader angewiesen sind, navigieren können.
describe("MyComponentComponent", () => {
it("should render initially with heading and tip of the day", async () => {
…
const hideButton = screen.getByRole("button", { name: "Ausblenden" });
expect(hideButton).toBeVisible();
expect(hideButton).not.toBeDisabled();
});
});
Test mit Interaktionen
Im zweiten Test prüfen wir mit userEvents
und unserem simulierten User, ob der Tipp des Tages bei einem Klick auf den Button auch verschwindet. Hierbei finden wir wie gewohnt erstmal den Aufruf unseres Setups vor. Danach prüfen wir den Zustand vor der Interaktion mithilfe von toBeVisible
und not.toBeInTheDocument
.
describe("MyComponentComponent", () => {
it("should render with heading and personal section initially", async () => {
…
});
it("should hide tip of the day on button click", async () => {
// GIVEN
const { user } = await setup();
// THEN
expect(screen.getByRole("heading", { level: 2, name: "Tipp des Tages" })).toBeVisible();
expect(screen.getByText("Testing Library macht Refactorings sicherer.")).toBeVisible();
expect(
screen.queryByRole("heading", { level: 2, name: "Du hast den Tipp des Tages schon gesehen." })
).not.toBeInTheDocument();
});
});
Anschließend erfolgt der Klick auf den Button. Diesen warten wir über das Schlüsselwort await
ab. Dadurch erhalten wir die UI im aktualisierten Zustand direkt im Screen – ohne uns um die Reaktivität kümmern zu müssen. Zum Abschluss prüfen wir, ob der „Tipp des Tages“ verschwunden ist und stattdessen der Hinweis auf den bereits gesehenen Tipp angezeigt wird. Auch der Button sollte nun deaktiviert sein und keine weiteren Interaktionen mehr zulassen.
describe("MyComponentComponent", () => {
it("should render with heading and personal section initially", async () => {
…
});
it("should hide tip of the day on button click", async () => {
…
// WHEN
const hideButton = screen.getByRole("button", { name: "Ausblenden" });
await user.click(hideButton);
// THEN
expect(screen.queryByRole("heading", { level: 2, name: "Tipp des Tages" })).not.toBeInTheDocument();
expect(screen.queryByRole("Testing Library macht Refactorings sicherer.")).toBeVisible();
expect(screen.getByRole("heading", { level: 2, name: "Du hast den Tipp des Tages schon gesehen." })).toBeVisible();
expect(hideButton).toBeDisabled();
});
});
Fazit
Insgesamt lässt sich sagen, dass die Testing Library immer stärker ins Zentrum von modernen Softwareentwicklungsprojekten rückt. Sie ermöglicht es, Tests zu schreiben, die realitätsnah und aussagekräftig sind, was wiederum Vertrauen in die Qualität der Software schafft. Die Testing Library, hat dazu beigetragen, den Fokus auf User-zentriertes Testen und Barrierefreiheit vermehrt in den Vordergrund zu rücken und hilft so Entwicklungsteams, qualitativ hochwertige und benutzungsfreundliche Anwendungen zu schaffen. Manche Tests mögen vielleicht langsamer als striktes Unit Testing sein, bieten dafür jedoch eine höhere Aussagekraft.
Wichtig ist hierbei, dass es kein „richtig“ oder „falsch“ gibt. Die Tests sollten immer auf die Anforderungen des Produkts und des Entwicklungsteams abgestimmt sein. Am Ende geht es darum, dass die Tests Vertrauen in die Funktionalität und Zuverlässigkeit der Software schaffen.
Quellen:
https://kentcdodds.com/
https://testing-library.com/
Softwareentwicklung
Erfahren Sie mehr über unsere Leistungen