Testen
Auftrag
Martina Hostettler meldet einen alten Fehler. Nach einer Änderung an den Regeln nimmt die App wieder Anfragen an, deren erster Tag schon vorbei ist.
Als Vorstand möchte ich, dass ein behobener Fehler nicht zurückkommt, damit sich Mitglieder und Team auf die Regeln der App verlassen können.
Der Freiwillige findet in den Tests der App keinen, der einen Tag in der Vergangenheit prüft. Die Akzeptanzkriterien lauten:
- Die Regeln einer Anfrage haben Tests, die jeden Fehler bemerken, der schon einmal aufgetreten ist.
- Komponenten haben Tests, die prüfen, was jemand sieht und was sie melden.
- Stores haben Tests, die eine Antwort der API vorgeben, ohne Server.
Lernziele
Nach diesem Kapitel kannst du
- Arten von Tests und ihren Zweck unterscheiden,
- Funktionen mit Vitest testen,
- Komponenten mit Vue Test Utils über ihr Verhalten testen,
- Stores und Composables testen und
fetchdabei ersetzen, - entscheiden, was sich zu testen lohnt, und deine Tests mit Mutanten prüfen.
Arten von Tests
Abschnitt betitelt „Arten von Tests“Die Vue-Dokumentation unterscheidet drei Arten. Unit-Tests prüfen eine Funktion, ein Composable oder einen Store für sich, sie empfiehlt dafür Vitest. Komponententests zeigen eine Komponente und prüfen, was sie darstellt und meldet, mit Vitest und Vue Test Utils. End-to-End-Tests steuern die ganze App in einem echten Browser, dafür nennt sie Playwright und Cypress. Alle Übungsprojekte dieses Kurses haben Tests der ersten beiden Arten, die du seit Teil 3 mit npm test startest.
Eine Funktion testen
Abschnitt betitelt „Eine Funktion testen“Ein Test ruft eine Funktion mit bekannten Werten auf und vergleicht ihr Ergebnis mit expect. describe fasst Tests zusammen, it beschreibt einen Test in einem Satz. Die Regeln einer Anfrage haben in der Fallbeispiel-App jetzt einen Test für den Fehler, der zurückgekommen ist.
describe('Regeln der Reservationsanfrage', () => { const check = (request, item = drill) => validateRequest({ from: '', to: '', ...request }, item, monday)
it('nimmt eine Anfrage von Mittwoch bis Samstag an', () => { expect(check({ from: '2026-11-18', to: '2026-11-21' })).toEqual({}) })
it('lehnt Tage vor der Rückgabe und mehr als sieben Tage ab', () => { expect(check({ from: '2026-11-11', to: '2026-11-14' }, tent)).toHaveProperty('from') expect(check({ from: '2026-11-18', to: '2026-11-28' })).toHaveProperty('to') })
// The mistake came back once after a change, this test keeps it from coming back again it('lehnt einen ersten Tag in der Vergangenheit ab', () => { expect(check({ from: '2026-10-31', to: '2026-11-04' })).toEqual({ from: 'Der erste Tag liegt in der Vergangenheit.' }) })})Der Tag steht als Argument im Test, darum gibt der Test morgen dasselbe Ergebnis wie heute. Der zweite Test prüft zehn Tage, eine falsche Grenze von acht oder neun Tagen bemerkt er nicht. Grenzen prüft ein Test darum auf beiden Seiten, gerade noch erlaubt und gerade nicht mehr.
Eine Komponente testen
Abschnitt betitelt „Eine Komponente testen“Vue Test Utils zeigt eine Komponente mit mount, findet Elemente mit find, füllt Felder mit setValue und löst Events mit trigger aus. Beide geben ein Promise zurück, und erst nach await steht der neue Stand im DOM. Was eine Komponente gemeldet hat, steht in emitted.
async function fill(from, to) { const wrapper = mount(RequestForm, { props: { today: '2026-11-02' } }) await wrapper.find('#from').setValue(from) await wrapper.find('#to').setValue(to) return wrapper}
describe('Vertiefung, das Formular', () => { it('sendet eine gültige Anfrage als Event', async () => { const wrapper = await fill('2026-11-18', '2026-11-21') await wrapper.find('form').trigger('submit') expect(wrapper.emitted('send')).toEqual([[{ from: '2026-11-18', to: '2026-11-21' }]]) })
it('sendet eine Anfrage mit Fehlern nicht', async () => { const wrapper = await fill('2026-11-18', '2026-11-28') await wrapper.find('form').trigger('submit') expect(wrapper.emitted('send')).toBeUndefined() expect(wrapper.find('.error').text()).toBe('Eine Ausleihe dauert höchstens sieben Tage.') })Laut Vue-Dokumentation prüft ein Komponententest die öffentliche Schnittstelle, also Props, Slots und Events, und was danach im DOM steht. Interne Variablen prüft er nicht, sonst bricht er bei jedem Umbau, auch wenn die Komponente noch tut, was sie soll. Auch ein Schnappschuss des HTML allein sagt nach der Dokumentation nicht, ob die Komponente richtig arbeitet.
Einen Store testen und fetch ersetzen
Abschnitt betitelt „Einen Store testen und fetch ersetzen“Ein Store braucht für jeden Test ein frisches Pinia, wie in Kapitel 7.2. fetch ersetzt der Test mit vi.stubGlobal durch eine Funktion von vi.fn, die eine vorbereitete Antwort liefert und jeden Aufruf notiert. So prüft er Adresse, Methode, Kopfzeilen und Inhalt, ohne dass ein Server läuft.
import { createPinia, setActivePinia } from 'pinia'import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRequestsStore } from '@/stores/requests.js'
const REQUEST = { itemId: 6, from: '2026-11-18', to: '2026-11-21' }
// The API answers with this status and bodyfunction answer(status, body) { const fetch = vi.fn(async () => new Response(JSON.stringify(body), { status })) vi.stubGlobal('fetch', fetch) return fetch}
beforeEach(() => setActivePinia(createPinia()))afterEach(() => vi.unstubAllGlobals())
describe('Knobelei, der Store mit der API', () => { it('schickt die Anfrage als JSON mit POST an die API', async () => { const fetch = answer(201, { id: 'R-1', ...REQUEST }) await useRequestsStore().send(REQUEST) const [url, options] = fetch.mock.calls[0] expect(url).toBe('/api/de/reservations') expect(options.method).toBe('POST') expect(options.headers['Content-Type']).toBe('application/json') expect(JSON.parse(options.body)).toEqual(REQUEST) })vi.unstubAllGlobals stellt nach jedem Test das echte fetch wieder her, sonst wirkt die Attrappe in den nächsten Test hinein.
Composables testen
Abschnitt betitelt „Composables testen“Ein Composable, das nur ref, computed und watch verwendet, ruft ein Test direkt auf und prüft die zurückgegebenen Refs, wie die Tests zu Kapitel 5.4. Verwendet es Hooks wie onMounted oder inject, braucht es laut Vue-Dokumentation eine Komponente als Wirt. Die Dokumentation zeigt dafür eine kleine Funktion, die eine App nur für das Composable anlegt und montiert.
Die App im Test
Abschnitt betitelt „Die App im Test“Die Tests der Fallbeispiel-App zeigen die ganze App mit Router, Pinia und der API und ersetzen nur fetch durch eine Attrappe der Kurs-API in tests/fake-api.js. Eine Hilfsfunktion führt zu einer Adresse, wie nach dem Eintippen im Browser.
// Pinia with the API, as main.js sets it up. A small app gives Pinia the API before the first// navigation, because a guard may create a store before App is mounted.export function createTestPinia(adapter = createHttpApi('/api/de')) { const pinia = createPinia() createApp({}).provide(apiKey, adapter).use(pinia) setActivePinia(pinia) return { pinia, adapter }}
// Mounts App with the router, Pinia and the same APIexport function mountApp({ pinia, adapter }, options = {}) { return mount(App, { global: { plugins: [router, pinia], provide: { [apiKey]: adapter } }, ...options })}
// Goes to an address and shows App there, as after typing the address into the browser, with a// fresh Pinia and a stand-in for the course API. member signs someone in beforehand, as the// session cookie of an earlier visit would.export async function visit(path, { member = null, requests = [], ...options } = {}) { api = fakeApi({ member, requests }) const setup = createTestPinia() await router.push(path) const wrapper = mountApp(setup, options) await flushPromises() return wrapper}Solche Tests prüfen das Zusammenspiel von Seiten, Stores und Router und brauchen trotzdem keinen Browser. Sie laufen in happy-dom, einer Nachbildung des Browsers in Node, die nichts zeichnet. Ob ein Knopf sichtbar über einem anderen liegt, prüft erst ein End-to-End-Test.
Tests, die sich lohnen
Abschnitt betitelt „Tests, die sich lohnen“Am meisten bringen Tests für Regeln mit Grenzen, für Fehlerzustände, für das, was Mitglieder sehen, und für jeden Fehler, der schon einmal aufgetreten ist. Ein solcher Test entsteht am besten vor der Korrektur. Er scheitert erst, und nach der Korrektur besteht er. Wenig bringen Tests, die Vue selbst oder eine Bibliothek prüfen, und Tests, die jede interne Zeile festschreiben.
Tests prüfen mit Mutanten
Abschnitt betitelt „Tests prüfen mit Mutanten“Die Abdeckung zeigt, welche Zeilen ein Test ausgeführt hat. Vitest misst sie mit dem Paket @vitest/coverage-v8 und vitest run --coverage, laut Vitest-Dokumentation. Ob ein Test einen Fehler in diesen Zeilen bemerken würde, zeigt sie nicht. Das prüft ein Mutationstest. Er baut kleine Fehler in den Code ein, sogenannte Mutanten, und schaut, ob die Tests danach scheitern. Ein Mutant, der alle Tests besteht, zeigt eine Lücke. Werkzeuge wie StrykerJS tun das für ganze Projekte. Das Übungsprojekt dieses Kapitels bringt eine kleine Fassung davon mit.
In deinem Projekt
Abschnitt betitelt „In deinem Projekt“Der Mindestumfang verlangt drei automatisierte Tests, zu zweit sechs. Wähl sie so, dass sie etwas Wichtiges prüfen, etwa eine Regel deiner App, eine Komponente mit Verhalten und einen Store mit ersetztem fetch. Schreib einen Test, sobald du einen Fehler behebst. Der Screencast fragt nach dem Fehler, der dich am längsten beschäftigt hat, und ein Test dazu zeigt, dass du ihn verstanden hast.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit der Fallbeispiel-App im Kursrepo im Ordner case/08-architecture/de und mit dem Übungsprojekt dieses Kapitels. Schreib auf, was du erwartest, bevor du etwas ausführst.
- Setz in
src/features/reservations/rules.jsdie GrenzeMAX_DAYSauf9und starte die Tests. Bemerkt einer den Fehler? Warum nicht? - Nimm im zweiten Komponententest der Lösung das
awaitvortrigger('submit')weg. Was passiert, und warum? - Starte im Übungsprojekt
npm test, bevor du Tests schreibst. Was meldet die Prüfung? - Ein Test vergleicht nur
wrapper.html()mit einem gespeicherten Schnappschuss. Welche Fehler findet er, und welche nicht?
Aufgaben
Abschnitt betitelt „Aufgaben“In diesem Projekt schreibst du die Tests selbst. Du findest es im Kursrepo im Ordner exercises/08-architecture/testing/de, mit Regeln für Anfragen in src/rules.js, einem Formular und einem Store. npm test lässt deine Tests zuerst gegen den richtigen Code laufen, dort müssen sie bestehen. Danach laufen die Tests jeder Stufe gegen Mutanten, und npm test meldet, wie viele Fehler deine Tests finden. Mit npm run test:watch laufen nur deine Tests, bei jeder Änderung neu.
npm installnpm testAufgabe Einstieg
Schreib in tests/level-1.test.js Tests für validateRequest. Eine Anfrage über sieben Tage ist gültig, eine über acht nicht, ein erster Tag in der Vergangenheit nicht und ein letzter Tag vor dem ersten auch nicht. Wähl den heutigen Tag im Test selbst. Deine Tests finden alle drei Mutanten dieser Stufe.
Lösung
import { describe, expect, it } from 'vitest'
import { validateRequest } from '@/rules.js'
// Monday, 2 November 2026const today = '2026-11-02'const check = (from, to) => validateRequest({ from, to }, today)
describe('Einstieg, die Regeln einer Anfrage', () => { it('nimmt eine Anfrage über sieben Tage an', () => { expect(check('2026-11-18', '2026-11-25')).toEqual({}) })
it('lehnt eine Anfrage über acht Tage ab', () => { expect(check('2026-11-18', '2026-11-26')).toHaveProperty('to') })
it('lehnt einen ersten Tag in der Vergangenheit ab', () => { expect(check('2026-11-01', '2026-11-04')).toHaveProperty('from') })
it('lehnt einen letzten Tag vor dem ersten ab', () => { expect(check('2026-11-18', '2026-11-14')).toHaveProperty('to') })})Aufgabe Vertiefung
Schreib in tests/level-2.test.js Tests für RequestForm.vue. Eine gültige Anfrage meldet das Formular als Event send, eine Anfrage mit Fehlern nicht, und Meldungen erscheinen erst nach dem ersten Senden. Deine Tests finden alle drei Mutanten dieser Stufe.
Lösung
import { mount } from '@vue/test-utils'import { describe, expect, it } from 'vitest'
import RequestForm from '@/components/RequestForm.vue'
async function fill(from, to) { const wrapper = mount(RequestForm, { props: { today: '2026-11-02' } }) await wrapper.find('#from').setValue(from) await wrapper.find('#to').setValue(to) return wrapper}
describe('Vertiefung, das Formular', () => { it('sendet eine gültige Anfrage als Event', async () => { const wrapper = await fill('2026-11-18', '2026-11-21') await wrapper.find('form').trigger('submit') expect(wrapper.emitted('send')).toEqual([[{ from: '2026-11-18', to: '2026-11-21' }]]) })
it('sendet eine Anfrage mit Fehlern nicht', async () => { const wrapper = await fill('2026-11-18', '2026-11-28') await wrapper.find('form').trigger('submit') expect(wrapper.emitted('send')).toBeUndefined() expect(wrapper.find('.error').text()).toBe('Eine Ausleihe dauert höchstens sieben Tage.') })
it('zeigt Meldungen erst nach dem Senden', async () => { const wrapper = await fill('', '') expect(wrapper.find('.error').exists()).toBe(false) await wrapper.find('form').trigger('submit') expect(wrapper.findAll('.error')).toHaveLength(2) })})Aufgabe Knobelei
Schreib in tests/level-3.test.js Tests für den Store useRequestsStore, mit einem frischen Pinia und einem ersetzten fetch. Prüf Adresse, Methode, Kopf und Inhalt der Anfrage, eine angenommene Anfrage in der Liste und eine Ablehnung der API mit ihrer Meldung. Deine Tests finden alle drei Mutanten dieser Stufe.
Lösung
import { createPinia, setActivePinia } from 'pinia'import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRequestsStore } from '@/stores/requests.js'
const REQUEST = { itemId: 6, from: '2026-11-18', to: '2026-11-21' }
// The API answers with this status and bodyfunction answer(status, body) { const fetch = vi.fn(async () => new Response(JSON.stringify(body), { status })) vi.stubGlobal('fetch', fetch) return fetch}
beforeEach(() => setActivePinia(createPinia()))afterEach(() => vi.unstubAllGlobals())
describe('Knobelei, der Store mit der API', () => { it('schickt die Anfrage als JSON mit POST an die API', async () => { const fetch = answer(201, { id: 'R-1', ...REQUEST }) await useRequestsStore().send(REQUEST) const [url, options] = fetch.mock.calls[0] expect(url).toBe('/api/de/reservations') expect(options.method).toBe('POST') expect(options.headers['Content-Type']).toBe('application/json') expect(JSON.parse(options.body)).toEqual(REQUEST) })
it('übernimmt eine angenommene Anfrage in die Liste', async () => { answer(201, { id: 'R-1', ...REQUEST }) const store = useRequestsStore() expect(await store.send(REQUEST)).toBe(true) expect(store.list).toEqual([{ id: 'R-1', ...REQUEST }]) })
it('meldet eine Ablehnung und übernimmt nichts', async () => { answer(409, { error: 'Das Ding ist an einigen dieser Tage schon angefragt.' }) const store = useRequestsStore() expect(await store.send(REQUEST)).toBe(false) expect(store.list).toEqual([]) expect(store.error).toBe('Das Ding ist an einigen dieser Tage schon angefragt.') })})Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
| Ein Test besteht immer, auch wenn der Code falsch ist. | Den Test einmal mit absichtlich falschem Code laufen lassen, er muss scheitern. |
| Ein Komponententest prüft interne Variablen und bricht bei jedem Umbau. | Prüfen, was die Komponente zeigt und meldet. |
Nach setValue oder trigger sieht der Test noch den alten Stand. | await davor setzen und nach Anfragen flushPromises aufrufen. |
| Ein Test ruft den echten Server auf und scheitert, wenn er nicht läuft. | fetch mit vi.stubGlobal ersetzen und nach dem Test mit vi.unstubAllGlobals zurücksetzen. |
| Tests beeinflussen sich über einen Store. | Vor jedem Test ein frisches Pinia mit setActivePinia(createPinia()). |
| Ein KI-Assistent schreibt Tests, die nur nachbilden, was der Code gerade tut. | Erwartungen aus den Anforderungen ableiten und jeden Test einmal gegen einen Fehler laufen lassen. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Welche Arten von Tests unterscheidet die Vue-Dokumentation, und womit schreibt man sie?
- Was prüft ein Komponententest, und was nicht?
- Wie testet ein Test einen Store, der
fetchaufruft, ohne Server? - Was zeigt ein Mutant, das die Abdeckung nicht zeigt?
Antworten
- Unit-Tests und Komponententests mit Vitest und Vue Test Utils, End-to-End-Tests mit Playwright oder Cypress.
- Was die Komponente darstellt und meldet, nach Props, Slots und Eingaben. Interne Variablen prüft er nicht.
- Mit einem frischen Pinia und einem
fetch, dasvi.stubGlobaldurch eine Attrappe mit vorbereiteter Antwort ersetzt. - Ob ein Test einen Fehler in einer ausgeführten Zeile auch bemerkt.
Auftrag
Der Fehler mit dem Tag in der Vergangenheit hat jetzt einen Test, der ihn bemerkt. Wenig später schreibt ein Mitglied, das mit einem Screenreader arbeitet, an den Verein. Die Zahl der Treffer im Katalog bleibe stumm, alle Knöpfe «Favorit» klängen gleich, und nach dem Zurück-Knopf lande der Fokus im Suchfeld statt beim Ding. Das nächste Kapitel kümmert sich um die Barrierefreiheit.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.