echte Komponenten im echten Browser prüfen
Component Testing mit Cypress schließt die Lücke zwischen isolierten Unit-Tests und langsamen End-to-End-Läufen. Vue-Komponenten werden dabei tatsächlich im Browser gemountet, mit echtem DOM, echten Events und kontrollierbaren Netzwerkaufrufen, ohne die gesamte Anwendung hochfahren zu müssen.
Inhaltsverzeichnis
- 1. Component Testing zwischen Unit- und E2E-Test
- 2. Cypress Component Testing für Vue 3 einrichten
- 3. Die Mount-API: Props, Events und Slots
- 4. Interaktionen simulieren: Klicks, Formulare, Tastatur
- 5. Netzwerk-Requests mocken mit cy.intercept
- 6. Slots und Scoped Slots gezielt testen
- 7. Custom Commands für wiederkehrende Prüfungen
- 8. Component Testing in der CI-Pipeline
- 9. Cypress Component Testing im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Component Testing zwischen Unit- und E2E-Test
Component Testing mit Cypress besetzt eine Nische, die klassische Unit-Tests mit Vitest und Testing Library nicht abdecken können: Die Komponente wird nicht in einer simulierten JSDOM-Umgebung gerendert, sondern in einem echten, isolierten Browser-Tab. Das bedeutet, echtes Layout, echtes CSS, echte Event-Propagation und echte Browser-APIs stehen zur Verfügung, während gleichzeitig nur eine einzelne Komponente getestet wird, nicht die gesamte Anwendung mit Routing, Server und Datenbank. Für Vue-Projekte mit komplexen visuellen Zuständen, Drag-and-Drop-Interaktionen oder CSS-Transitions ist das ein reeller Mehrwert gegenüber JSDOM-basierten Tests.
Der entscheidende Unterschied zu einem vollständigen End-to-End-Test liegt im Scope. Ein E2E-Test mit Cypress oder Playwright startet die komplette Anwendung, navigiert durch Seiten und prüft ganze User-Flows. Component Testing dagegen mountet eine einzelne Vue-Komponente direkt, mit expliziten Props und Mocks für alle externen Abhängigkeiten. Das Ergebnis sind Tests, die fast so schnell laufen wie Unit-Tests, aber die Zuverlässigkeit echter Browser-Rendering-Läufe besitzen. Genau dieser Mittelweg macht Component Testing für Design-Systeme, wiederverwendbare UI-Bibliotheken und komplexe Formulare in Vue-Anwendungen attraktiv.
In der Praxis zeigt sich häufig, dass Teams entweder ausschließlich auf Unit-Tests mit oberflächlichem Mounting setzen, oder ausschließlich auf träge E2E-Suiten. Component Testing mit Cypress füllt die Lücke dazwischen und wird in diesem Artikel Schritt für Schritt aufgebaut, vom Setup über Mocking bis zur Integration in die CI-Pipeline.
2. Cypress Component Testing für Vue 3 einrichten
Der Einstieg in Component Testing beginnt mit der Cypress-Installation und der Auswahl des richtigen Frameworks im Setup-Assistenten. Cypress erkennt bei einem Vite-basierten Vue-Projekt automatisch den passenden Dev-Server-Adapter und schlägt die Konfiguration @cypress/vue vor. Wichtig ist, die cypress.config.ts sauber von der E2E-Konfiguration zu trennen, da Component Testing einen eigenen Dev-Server startet und eine eigene Support-Datei nutzt.
Ein häufiger Stolperstein: Globale Plugins wie Pinia, Vue Router oder ein i18n-Setup, die in der echten Anwendung über app.use() registriert werden, fehlen im isolierten Component-Test-Kontext komplett. Deshalb definiert man in der Support-Datei einen zentralen mount-Wrapper, der die nötigen Plugins bereits vorkonfiguriert, damit einzelne Testdateien nicht bei jedem Test denselben Boilerplate wiederholen müssen.
// cypress.config.ts — Component Testing setup for a Vite-based Vue 3 project
import { defineConfig } from "cypress";
export default defineConfig({
component: {
devServer: {
framework: "vue",
bundler: "vite",
},
specPattern: "src/**/*.cy.{js,ts}",
supportFile: "cypress/support/component.ts",
},
});
// cypress/support/component.ts — global mount wrapper with plugins
import { mount } from "cypress/vue";
import { createPinia } from "pinia";
import { createRouter, createWebHistory } from "vue-router";
Cypress.Commands.add("mount", (component, options = {}) => {
options.global = options.global || {};
options.global.plugins = options.global.plugins || [];
// Always provide a fresh Pinia instance per test
options.global.plugins.push(createPinia());
// Only attach router when the test explicitly needs it
if (options.router !== false) {
const router = createRouter({
history: createWebHistory(),
routes: options.routes || [{ path: "/", component: { template: "<div/>" } }],
});
options.global.plugins.push(router);
}
return mount(component, options);
});
Diese zentrale Konfiguration sorgt dafür, dass jede einzelne Testdatei nur noch die Komponente und ihre spezifischen Props übergeben muss, während Pinia und Router automatisch verfügbar sind. Das reduziert Boilerplate erheblich und macht Component Testing mit Cypress im Alltag deutlich angenehmer, weil neue Tests in wenigen Zeilen geschrieben werden können, statt jedes Mal die komplette Plugin-Kette neu aufzubauen.
3. Die Mount-API: Props, Events und Slots
Das Herzstück von Component Testing ist der Befehl cy.mount(), der eine einzelne Vue-Komponente in einem isolierten iframe rendert. Über die props-Option werden Eingabewerte gesetzt, über global.provide lassen sich Werte für Composables injizieren, die auf provide/inject basieren. Nach dem Mounten verhält sich die Komponente exakt wie im echten Betrieb, inklusive reaktiver Updates, Watcher und Lifecycle-Hooks.
Ein zentraler Vorteil gegenüber Testing Library in JSDOM: Emitted Events lassen sich direkt über die zurückgegebene wrapper-Referenz abfragen, und CSS-Klassen, die durch echte Browser-Layout-Berechnungen entstehen (etwa display: none durch Media Queries), werden korrekt ausgewertet. Damit lassen sich auch responsive Komponenten testen, deren Verhalten von der tatsächlichen Viewport-Größe abhängt, was in einer simulierten DOM-Umgebung nicht zuverlässig möglich ist.
// ProductCard.cy.js — mounting a component with props and asserting emitted events
import ProductCard from "./ProductCard.vue";
describe("ProductCard", () => {
it("renders price and title from props", () => {
cy.mount(ProductCard, {
props: {
product: { id: 42, title: "Wireless Keyboard", price: 79.9, inStock: true },
},
});
cy.get("[data-testid='product-title']").should("contain.text", "Wireless Keyboard");
cy.get("[data-testid='product-price']").should("contain.text", "79,90");
});
it("emits add-to-cart with the product id when the button is clicked", () => {
const product = { id: 42, title: "Wireless Keyboard", price: 79.9, inStock: true };
cy.mount(ProductCard, { props: { product } }).then(({ wrapper }) => {
cy.get("[data-testid='add-to-cart']").click().then(() => {
expect(wrapper.emitted("add-to-cart")).to.have.length(1);
expect(wrapper.emitted("add-to-cart")[0]).to.deep.equal([42]);
});
});
});
it("disables the button when the product is out of stock", () => {
cy.mount(ProductCard, {
props: { product: { id: 7, title: "Sold Out Mouse", price: 29.9, inStock: false } },
});
cy.get("[data-testid='add-to-cart']").should("be.disabled");
});
});
4. Interaktionen simulieren: Klicks, Formulare, Tastatur
Weil Component Testing mit Cypress im echten Browser läuft, funktionieren Standard-Interaktionsbefehle wie cy.get().click(), cy.get().type() und cy.get().select() identisch wie in einer klassischen E2E-Suite. Das ist ein spürbarer Unterschied zu Testing Library, wo Nutzerinteraktionen über fireEvent oder userEvent synthetisch erzeugt werden müssen. Bei Cypress löst ein click()-Befehl tatsächliche Mausevents mit korrekten Koordinaten aus, was insbesondere bei Komponenten mit @click.stop, Overlay-Elementen oder Drag-Handlern realistischere Ergebnisse liefert.
Für komplexere Formulare in Vue empfiehlt sich, jede Eingabe mit einer expliziten Zusicherung des resultierenden Zustands zu kombinieren, statt nur den finalen Submit zu prüfen. So wird sichtbar, an welcher Stelle genau eine Validierung fehlschlägt, wenn ein Test rot wird, statt nur ein pauschales „Formular wurde nicht abgeschickt" zu melden.
// LoginForm.cy.js — simulating real keyboard and mouse interactions
import LoginForm from "./LoginForm.vue";
describe("LoginForm", () => {
it("shows a validation error for an invalid email", () => {
cy.mount(LoginForm);
cy.get("[data-testid='email-input']").type("not-an-email");
cy.get("[data-testid='password-input']").type("supersecret123");
cy.get("[data-testid='submit-button']").click();
cy.get("[data-testid='email-error']").should("contain.text", "gueltige E-Mail-Adresse");
});
it("submits valid credentials and emits the login event", () => {
cy.mount(LoginForm).then(({ wrapper }) => {
cy.get("[data-testid='email-input']").type("user@example.com");
cy.get("[data-testid='password-input']").type("supersecret123");
cy.get("[data-testid='submit-button']").click().then(() => {
expect(wrapper.emitted("login")).to.exist;
});
});
});
it("supports keyboard-only submission via Enter", () => {
cy.mount(LoginForm);
cy.get("[data-testid='email-input']").type("user@example.com");
cy.get("[data-testid='password-input']").type("supersecret123{enter}");
cy.get("[data-testid='submit-button']").should("have.attr", "aria-pressed", "true");
});
});
5. Netzwerk-Requests mocken mit cy.intercept
Component Testing isoliert eine Vue-Komponente von der restlichen Anwendung, aber viele Komponenten laden Daten selbst über fetch oder Composables wie useFetch. Genau hier setzt cy.intercept() an: Der Befehl fängt Netzwerkaufrufe auf Browser-Ebene ab, bevor sie das Backend erreichen, und antwortet mit vordefinierten Fixtures. Das macht Tests deterministisch, unabhängig von echten Servern, und erlaubt gezieltes Testen von Fehlerzuständen, die im echten Backend schwer reproduzierbar wären.
Ein wichtiger Unterschied zu Unit-Test-Mocking mit vi.mock(): cy.intercept() mockt auf der Netzwerkebene, nicht auf Modulebene. Das bedeutet, die tatsächliche Fetch-Implementierung der Komponente bleibt unangetastet und wird real ausgeführt, nur die Antwort kommt aus der Fixture. Dadurch werden auch Fehler in der eigentlichen Fetch-Logik der Komponente aufgedeckt, die bei reinem Modul-Mocking unbemerkt blieben.
// OrderList.cy.js — intercepting network calls at the browser level
import OrderList from "./OrderList.vue";
describe("OrderList", () => {
it("renders orders returned by the API", () => {
cy.intercept("GET", "/api/orders", {
statusCode: 200,
body: [
{ id: 1, status: "shipped", total: 129.5 },
{ id: 2, status: "processing", total: 49.0 },
],
}).as("getOrders");
cy.mount(OrderList);
cy.wait("@getOrders");
cy.get("[data-testid='order-row']").should("have.length", 2);
cy.get("[data-testid='order-row']").first().should("contain.text", "shipped");
});
it("shows an error state when the API responds with 500", () => {
cy.intercept("GET", "/api/orders", { statusCode: 500 }).as("getOrdersFailed");
cy.mount(OrderList);
cy.wait("@getOrdersFailed");
cy.get("[data-testid='error-banner']").should("be.visible");
cy.get("[data-testid='retry-button']").should("exist");
});
it("retries the request when the retry button is clicked", () => {
cy.intercept("GET", "/api/orders", { statusCode: 500 }).as("firstAttempt");
cy.mount(OrderList);
cy.wait("@firstAttempt");
cy.intercept("GET", "/api/orders", { statusCode: 200, body: [] }).as("retryAttempt");
cy.get("[data-testid='retry-button']").click();
cy.wait("@retryAttempt");
cy.get("[data-testid='empty-state']").should("be.visible");
});
});
6. Slots und Scoped Slots gezielt testen
Vue-Komponenten mit Slots stellen eine besondere Herausforderung für Component Testing dar, weil der eigentliche Inhalt vom Aufrufer bestimmt wird, nicht von der Komponente selbst. Die Mount-API von Cypress erlaubt es, Slots über die Option slots mit statischem Markup oder Render-Funktionen zu befüllen. Für Default-Slots reicht ein einfacher HTML-String, für Scoped Slots braucht es eine Funktion, die die vom Kind bereitgestellten Slot-Props entgegennimmt.
Der Test sollte in beiden Fällen prüfen, ob die Slot-Props korrekt an den Aufrufer durchgereicht werden, nicht nur ob der Slot-Inhalt überhaupt gerendert wird. Gerade bei Scoped Slots, die etwa Paginierungs- oder Sortier-Zustand nach außen geben, ist das die eigentliche Testaussage, die Fehler in der internen Logik der Komponente aufdeckt.
// DataTable.cy.js — testing default slots and scoped slots
import DataTable from "./DataTable.vue";
describe("DataTable slots", () => {
it("renders custom markup passed into the default slot", () => {
cy.mount(DataTable, {
props: { items: [] },
slots: {
empty: "<p data-testid='custom-empty'>Keine Eintraege gefunden</p>",
},
});
cy.get("[data-testid='custom-empty']").should("be.visible");
});
it("passes row data through the scoped slot to the caller", () => {
cy.mount(DataTable, {
props: { items: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }] },
slots: {
row: (props) => `<td data-testid="row-${props.item.id}">${props.item.name}</td>`,
},
});
cy.get("[data-testid='row-1']").should("contain.text", "Alice");
cy.get("[data-testid='row-2']").should("contain.text", "Bob");
});
});
7. Custom Commands für wiederkehrende Prüfungen
Sobald mehrere Testdateien dieselben Mount-Optionen, dieselben Interceptions oder dieselben Zusicherungen wiederholen, lohnt sich ein Custom Command. Cypress erlaubt es, in cypress/support/component.ts eigene Befehle zu registrieren, die die TypeScript-Typdefinitionen erweitern. Für Vue-Projekte sind Custom Commands besonders für Formular-Ausfüllhelfer, Login-Simulationen oder standardisierte Fixture-Loader sinnvoll.
Wichtig ist, Custom Commands nicht zu generisch zu gestalten. Ein Command, der zu viele Sonderfälle über Parameter abdeckt, wird selbst zu einer Fehlerquelle und schwer zu debuggen. Die bessere Praxis ist ein kleiner, klar benannter Command pro wiederkehrender Aufgabe, etwa cy.fillLoginForm() oder cy.mockOrdersApi(), statt eines einzigen allmächtigen Helfers.
// cypress/support/component.ts — reusable custom commands
Cypress.Commands.add("fillLoginForm", (email, password) => {
cy.get("[data-testid='email-input']").clear().type(email);
cy.get("[data-testid='password-input']").clear().type(password);
});
Cypress.Commands.add("mockOrdersApi", (orders = []) => {
cy.intercept("GET", "/api/orders", { statusCode: 200, body: orders }).as("ordersRequest");
});
// Usage inside a spec file
describe("Checkout flow", () => {
it("logs in and shows the order list", () => {
cy.mockOrdersApi([{ id: 1, status: "shipped", total: 99.0 }]);
cy.mount(App);
cy.fillLoginForm("user@example.com", "supersecret123");
cy.get("[data-testid='submit-button']").click();
cy.wait("@ordersRequest");
});
});
8. Component Testing in der CI-Pipeline
Component Testing entfaltet seinen vollen Nutzen erst, wenn es zuverlässig in der Continuous Integration läuft. Cypress bietet dafür den Headless-Modus über cypress run --component, der ohne sichtbare UI läuft und sich in GitHub Actions, GitLab CI oder jedem anderen Runner integrieren lässt. Für Vue-Monorepos empfiehlt sich, Component Tests parallel zu Unit-Tests laufen zu lassen, da beide unterschiedliche Fehlerklassen abdecken und sich nicht gegenseitig blockieren sollten.
Für stabile CI-Läufe ist entscheidend, dass keine echten externen Services angesprochen werden. Jede Komponente, die Netzwerkaufrufe tätigt, muss über cy.intercept() vollständig gemockt sein, sonst führen Netzwerklatenzen oder Ausfälle externer Dienste zu Flaky Tests. Screenshots und Videos bei fehlgeschlagenen Läufen, die Cypress automatisch erzeugt, beschleunigen die Fehlersuche im CI erheblich, weil Entwickler den exakten visuellen Zustand zum Fehlerzeitpunkt sehen, statt nur eine Stack-Trace-Zeile.
9. Cypress Component Testing im Vergleich
Component Testing mit Cypress ist eine von mehreren Optionen im Testing-Werkzeugkasten für Vue-Anwendungen. Die Wahl zwischen Cypress Component Testing, klassischem Unit Testing mit Vitest und vollständigem End-to-End-Testing hat direkte Auswirkungen auf Laufzeit, Realitätsnähe und Wartungsaufwand.
| Kriterium | Unit Test (Vitest + JSDOM) | Cypress Component Testing | Vollständiger E2E-Test |
|---|---|---|---|
| Rendering-Umgebung | Simuliertes DOM (JSDOM) | Echter Browser | Echter Browser |
| Testumfang | Einzelne Funktion / Komponente | Einzelne Komponente, isoliert | Gesamte Anwendung |
| CSS und Layout geprüft | Nein, nur Klassen | Ja, echtes Rendering | Ja, echtes Rendering |
| Laufzeit pro Test | Sehr schnell (ms) | Schnell (Sekunden) | Langsam (mehrere Sekunden) |
| Backend nötig | Nein | Nein, via cy.intercept | Ja, meist Testumgebung |
In der Praxis schließen sich diese drei Ansätze nicht gegenseitig aus, sondern ergänzen sich. Reine Logik, etwa Composables oder Utility-Funktionen, testet man am günstigsten mit Vitest. Komponenten mit komplexem visuellen Verhalten, CSS-abhängiger Logik oder Slot-Interaktionen profitieren am meisten von Cypress Component Testing. Vollständige Nutzer-Flows über mehrere Seiten hinweg gehören weiterhin in eine schlanke E2E-Suite, die bewusst klein gehalten wird, um Laufzeiten in der CI kontrollierbar zu halten.
Mironsoft
Vue-Testing-Strategien und stabile Test-Suiten für Frontend-Teams
Belastbare Component Tests für eure Vue-Anwendung?
Wir bauen Cypress Component Testing in bestehende Vue-Projekte ein, richten Mocking-Strategien mit cy.intercept ein und sorgen für stabile, aussagekräftige Test-Suiten statt flakiger E2E-Läufe.
Test-Setup
Cypress Component Testing für Vite und Vue 3 aufsetzen, inklusive Pinia und Router
Mocking-Strategie
Netzwerk-Mocking mit cy.intercept und Fixture-Strukturen für deterministische Tests
CI-Integration
Component Tests headless in bestehende Pipelines integrieren und parallelisieren
10. Zusammenfassung
Component Testing mit Cypress schließt die Lücke zwischen isolierten Unit-Tests und trägen End-to-End-Suiten, indem es Vue-Komponenten einzeln, aber im echten Browser rendert. cy.mount() ersetzt dabei das klassische Shallow-Mounting, cy.intercept() übernimmt deterministisches Netzwerk-Mocking, und Slots lassen sich über die Mount-Optionen gezielt mit statischem oder dynamischem Inhalt befüllen. Custom Commands reduzieren Boilerplate über viele Testdateien hinweg, ohne die Lesbarkeit einzelner Tests zu opfern.
Der größte Hebel liegt darin, Component Testing nicht als Ersatz, sondern als Ergänzung zu Unit- und E2E-Tests zu verstehen. Reine Logik gehört in schnelle Vitest-Suiten, komplettes Nutzerverhalten über mehrere Seiten in eine schlanke E2E-Suite, und alles dazwischen, insbesondere visuell komplexe oder slot-basierte Komponenten, profitiert am meisten von Cypress Component Testing. Wer diese drei Ebenen bewusst trennt, bekommt eine Test-Pyramide, die sowohl schnell als auch aussagekräftig ist.
Component Testing mit Cypress — Das Wichtigste auf einen Blick
Mount-API
cy.mount() rendert eine einzelne Vue-Komponente im echten Browser, mit Props, Events und Slots wie im Produktivbetrieb.
Netzwerk-Mocking
cy.intercept() mockt auf Netzwerkebene, nicht auf Modulebene, und deckt so echte Fetch-Fehler in der Komponente auf.
Slots und Scoped Slots
Über die slots-Option lassen sich sowohl statisches Markup als auch Render-Funktionen für Scoped Slots übergeben.
CI-Tauglichkeit
Headless-Modus mit cypress run --component, vollständiges Mocking externer Aufrufe verhindert Flaky Tests in der Pipeline.