Jeg stolede på min SPA uden tests, det gik som forventet

Jeg stolede på min SPA uden tests, det gik som forventet

Du sidder med din første lille single page app, klikker lidt rundt, alt virker, og du tænker: “Fint, den er der”. Du deployer til Netlify eller Vercel, sender linket til en ven, og tre minutter senere får du en besked: “Øh, jeg kan ikke logge ind?”.

Det var her, jeg første gang tænkte: Jeg bliver nødt til at have noget, der klikker rundt for mig, hver gang jeg ændrer noget. Det blev Playwright.

Hvad jeg gerne vil have Playwright til at gøre for mig

Jeg bruger Playwright som en meget disciplineret test-bruger. Ikke til alt. Bare til nogle få, men vigtige ting:

1) Kan min app faktisk loade den side, jeg tror.
2) Kan man bruge de vigtigste flows uden at kende til min kode.
3) Overlever det en deploy, eller døde jeg på vejen.

I stedet for at tale længe om teori, starter jeg med et konkret mål. Lad os sige, at du har en simpel SPA med:

– En forside med en liste af todo-items
– Et login (fx fake, bare et felt og en knap)
– Mulighed for at tilføje og slette et todo-item

Det, jeg vil have Playwright til at teste, er:

– At forsiden loader, og at en bestemt tekst kan ses
– At login-flowet kan gennemføres
– At jeg kan oprette og slette et todo-item
– At det også virker på den rigtige, deployede URL

Hele pointen er, at du kan tage de samme mønstre med over i din egen app. Om det er en todo, et lille dashboard eller en lille læringsplatform, er ligegyldigt. Strukturen er den samme.

Første skridt med Playwright uden at drukne

Jeg tager udgangspunkt i en almindelig Node-opsætning. Du har en SPA, som du kan starte lokalt, typisk med npm run dev eller lignende.

Start med at installere Playwright i et eksisterende projekt:

npm init playwright@latest

Hvis du allerede har en package.json, spørger den, om den skal bruge den. Sig ja.

Du får et par spørgsmål. Mit typiske valg i små projekter:

? Where to put your end-to-end tests? > tests
? Add a GitHub Actions workflow? > No (kan komme senere)
? Install Playwright browsers? > Yes

Efter det har kørt, har du:

– En playwright.config.(ts|js)
– En tests-mappe med en eller flere eksempler
– Nye scripts i package.json, typisk:

"scripts": {
  "test:e2e": "playwright test",
  "test:e2e:headed": "playwright test --headed",
  "test:e2e:ui": "playwright test --ui"
}

Hvis du vil teste en SPA, der kører lokalt, skal du fortælle Playwright, hvor den skal gå hen. I playwright.config er der typisk en use-sektion:

use: {
  baseURL: 'http://localhost:5173',
}

Juster den til din dev-server. Vite bruger ofte port 5173, Create React App bruger 3000 osv.

Nu kan du allerede køre:

npm run test:e2e

Du vil se Playwright starte en browser i baggrunden, køre en demo-test og dumpe en rapport. Det er her, jeg som regel tænker: Fint, så fjerner jeg alt, hvad der følger med, og starter forfra med noget, jeg selv forstår.

Sådan gør du dine tests mindre skrøbelige med data-testid

De fleste, der prøver e2e første gang, gør det samme som jeg gjorde: bruger CSS-selectors i stil med .btn-primary eller .todo-item:nth-child(1). Det virker, lige indtil du ændrer lidt styling, eller listen sorteres anderledes. Så falder alt fra hinanden.

Min løsning i små projekter er at indføre en lille regel for mig selv:

– Alt, jeg vil teste med Playwright, får et data-testid-attribut.

Eksempel i din SPA:

<h1 data-testid="page-title">Min todo app</h1>

<form data-testid="todo-form">
  <input data-testid="todo-input" />
  <button data-testid="todo-submit">Tilføj</button>
</form>

<ul data-testid="todo-list">
  <li data-testid="todo-item">
    <span>Køb mælk</span>
    <button data-testid="todo-delete">Slet</button>
  </li>
</ul>

Det ændrer ikke noget for dine brugere, men det gør dit liv med tests meget nemmere. I testen kan du skrive:

await page.getByTestId('todo-input').fill('Køb kaffe');
await page.getByTestId('todo-submit').click();

Og du er fuldstændig ligeglad med, om knappen er blå eller grøn, om der er en ekstra <div> rundt om, eller om du har skiftet CSS-framework.

Hvis du vil læse lidt mere om den tilgang, har jeg også skrevet om semantik og struktur i HTML i en anden artikel på Coding Class, men lad os holde os til Playwright her.

De tre tests der redder mig oftest

Jeg skriver sjældent 50 e2e-tests. I små projekter ender jeg ofte med 3-6 stykker, som alle tjekker hele flows. Her er de tre, jeg starter med, næsten hver gang.

1. Navigationen og at siden faktisk loader

Den første test sørger bare for, at appen kan åbnes, og at en bestemt tekst eller komponent faktisk er der.

// tests/smoke.spec.ts
import { test, expect } from '@playwright/test';

test('forsiden loader med titel', async ({ page }) => {
  await page.goto('/');

  const title = page.getByTestId('page-title');
  await expect(title).toHaveText('Min todo app');
});

Nogle ting, der er værd at lægge mærke til:

– Jeg bruger page.goto('/') i stedet for en fuld URL. Det bruger baseURL fra config.
– Jeg bruger getByTestId, så testen er uafhængig af HTML-struktur.
– Jeg bruger expect(...).toHaveText(), som automatisk venter lidt, hvis elementet er forsinket.

Den her test fanger overraskende meget. Forkert route, bundlet der ikke loader, JavaScript-fejl på forsiden. Alt det, din mavefornemmelse ellers plejer at ignorere.

2. Formular med input og validering

De fleste SPA’er lever af formularer. Login, søgning, tilføj noget, rediger noget. Så jeg laver næsten altid én test, der gennemspiller en formular fra tom til gyldig.

Antag, at du har et login med en simpel validering:

<form data-testid="login-form">
  <input data-testid="login-email" type="email" />
  <input data-testid="login-password" type="password" />
  <button data-testid="login-submit">Log ind</button>
  <p data-testid="login-error"></p>
</form>

Playwright-testen kunne se sådan her ud:

test('login viser fejl og accepterer korrekt login', async ({ page }) => {
  await page.goto('/login');

  await page.getByTestId('login-submit').click();

  await expect(page.getByTestId('login-error'))
    .toHaveText('Udfyld begge felter');

  await page.getByTestId('login-email').fill('test@example.com');
  await page.getByTestId('login-password').fill('hemmelig');
  await page.getByTestId('login-submit').click();

  await expect(page.getByTestId('page-title'))
    .toHaveText('Min todo app');
});

Her tester du faktisk flere ting på én gang:

– At valideringstekst dukker op, når felter er tomme
– At valideringen forsvinder, når du udfylder korrekt
– At navigationen efter login virker

Hvis du en dag laver knappen om til et ikon eller ændrer teksten på fejlbeskeden, er det stadig den samme data-testid, så testen kan holde til det.

3. CRUD-flow i en lille todo-liste

Den tredje test kører hele vejen rundt: opret noget, se det, slet det igen. Det føles altid lidt tilfredsstillende, når den lykkes.

test('todo kan oprettes og slettes', async ({ page }) => {
  await page.goto('/');

  const input = page.getByTestId('todo-input');
  const submit = page.getByTestId('todo-submit');
  const list = page.getByTestId('todo-list');

  await input.fill('Køb kaffe');
  await submit.click();

  const newItem = list.getByText('Køb kaffe');
  await expect(newItem).toBeVisible();

  const deleteButton = newItem.getByTestId('todo-delete');
  await deleteButton.click();

  await expect(list.getByText('Køb kaffe')).toHaveCount(0);
});

Her bruger jeg en blanding af tekst og data-testid. Hvis du vil være helt stabil over for oversættelser eller tekstændringer, kan du give selve elementet et testid, fx data-testid="todo-item-text", og så finde det den vej.

Pointen er, at du tester et helt brugerflow i stedet for en enkelt komponent. Det er også der, e2e-skiller sig fra unit tests. Unit tests kan tjekke små funktioner. E2e fortæller dig, om en rigtig bruger kan gennemføre noget meningsfuldt.

Sådan undgår du at drukne i timeouts og flaky tests

Det hurtigste sted at miste tålmodigheden med e2e-tests er, når det hele fejler tilfældigt. Nogle gange lykkes det. Andre gange fejler det. Typisk med en eller anden timeout-fejl.

Der er to ting, der har hjulpet mig meget:

– Brug Playwrights egne locators i stedet for waitForTimeout
– Brug de indbyggede forventninger, som selv venter (til en grænse)

Dårligt eksempel:

await page.goto('/');
await page.waitForTimeout(2000);
const title = await page.$('h1');

Hvis din app en dag er langsommere end 2 sekunder, fejler testen. Hvis den er hurtigere, spilder du tid. Brug i stedet locators og forventninger:

await page.goto('/');
const title = page.getByTestId('page-title');
await expect(title).toHaveText('Min todo app');

Her vil Playwright selv prøve at finde elementet igen og igen i nogle sekunder og først fejle, hvis det virkelig ikke dukker op.

Hvis du arbejder med netværkskald, kan du også bruge page.waitForLoadState('networkidle') efter navigation, men ofte er locators + forventninger nok i simple SPA’er.

En anden typisk fejl er at skrive tests, der afhænger af data, som var der “tilfældigt”. Hvis din todo-liste fx loader nogle default-items fra en API, og du sletter den første, kan du ikke altid regne med, hvad der står.

Mit trick er:

– Opret altid dine egne test-data i starten af testen, inden du tjekker på dem.

Det er grunden til, at jeg i todo-eksemplet først tilføjer “Køb kaffe” og derefter leder efter lige præcis den tekst.

Smoke test på din rigtige, deployede URL

Det, der for alvor gjorde Playwright nyttigt for mig, var, da jeg stoppede med kun at teste lokalt. Det er hyggeligt, at tingene virker på localhost. Det er bare ikke altid det samme som produktion.

Jeg laver tit en separat Playwright-config kun til prod. For eksempel:

// playwright.prod.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: 'https://min-todo-app.vercel.app',
  },
  reporter: 'html',
});

Så kan jeg køre mine tests direkte mod den URL:

npx playwright test --config=playwright.prod.config.ts

De samme tests, men nu klikker de rundt på den rigtige side. Det fanger fejl som:

– Environment-variabler, der ikke er sat i produktion
– Bygget, der fejlede, men hvor hosten stadig viser en gammel version
– API-URL, der peger på localhost i stedet for prod

Hvis du en dag får lyst, kan du smide det ind i CI, så det kører automatisk efter deploy. På GitHub Actions kan du fx lave et simpelt workflow, der:

– Kører, når du pusher til main
– Deploy’er din app til Netlify / Vercel
– Kører npx playwright test --config=playwright.prod.config.ts

Men du behøver ikke starte der. Jeg kørte mine smoke tests manuelt længe, især på små projekter:

npm run build
npm run deploy
npx playwright test --config=playwright.prod.config.ts

Det tager et minut, og så ved du, om du skal sende linket videre til andre mennesker med god samvittighed.

Fejlsøgning med screenshots, video og traces

Når en e2e-test fejler, er det nemt bare at bande og køre den igen i håbet om, at den “lige var uheldig”. Det virker sjældent i længden.

Playwright har tre ting, jeg bruger meget, når en test driller:

– Video af test-run
– Screenshots ved fejl
– Traces, hvor du kan gå klik for klik igennem testen

I din playwright.config kan du slå det til:

use: {
  baseURL: 'http://localhost:5173',
  screenshot: 'only-on-failure',
  video: 'retain-on-failure',
  trace: 'retain-on-failure',
}

Når en test fejler, lægger Playwright filer i test-results-mappen. En typisk aften hos mig er:

– Jeg kører tests
– En fejler
– Jeg åbner HTML-rapporten med:

npx playwright show-report

Herfra kan du klikke dig ind på den fejlede test, se screenshot, se video, og åbne trace viewer. Trace viewer er især rar: den viser dig dommen over hvert klik, hvilket element den troede, den klikkede på, og hvilke fejl der dukkede op.

Hvis du gerne vil gå mere i dybden med debugging generelt, har jeg skrevet om debugging af JavaScript og console-fejl i en anden artikel på Coding Class, men Playwrights rapporter er et godt sted at starte i test-sammenhæng.

Næste skridt med Playwright i dine egne projekter

Hvis du har læst hertil, og du stadig synes, det her virker en anelse tungt, er min anbefaling faktisk ret simpel: start med bare én test.

Min typiske rækkefølge i et nyt lille projekt er:

– Sæt Playwright op og ret baseURL til
– Tilføj 2-3 data-testid-attributter på de vigtigste elementer
– Skriv én smoke test, der bare åbner forsiden og tjekker en tekst
– Først derefter de to andre tests (formular + CRUD)

Når du først har de tre tests kørende lokalt, er det ikke et stort skridt at kopiere config’en og pege den på din prod-URL. Pludselig har du en deploy smoke test, som du kan køre, hver gang du bygger noget nyt.

Hvis du også er nysgerrig på andre typer tests, kan du på et tidspunkt koble e2e sammen med unit tests eller integrationstests. Men selv hvis du aldrig kommer dertil, vil bare 3 Playwright-tests og en lille deploy smoke test allerede placere dig foran mange hobbyprojekter, der lever på ren held.

Jeg er nysgerrig på, hvornår du første gang opdager en fejl via en e2e-test, som du ellers ville have sendt videre til en rigtig bruger, og om det ændrer måden, du bygger dine små projekter på om et par år.

Gør baseURL konfigurerbart i playwright.config ved at læse en miljøvariabel, fx process.env.BASE_URL || 'http://localhost:5173'. Så kan du køre tests lokalt med standarden og i CI eller efter deploy sætte BASE_URL=https://dit-site.app npm run test:e2e.
Lav et lille script der logger ind via API, gemmer browserens storageState til en fil (fx auth.json), og genbrug den i tests med test.use({ storageState: 'auth.json' }). Det er hurtigere og mere pålideligt end at gentage UI-login i hver test.
Kør testen headed eller med PWDEBUG=1 for at se browseren live, aktiver trace eller video i config for at få artefakter, og åbn dem med playwright show-trace eller ved at se de gemte videoer/screenshots. Det giver trinvis genafspilning og konkrete fejlspor.
Tilføj en CI-job der installerer afhængigheder, bygger og enten deployer til et preview-miljø eller peger på den nyeste deployede URL, vent på at siden er oppe, og kør så npm run test:e2e. Lad jobbet fejle ved testfejl og upload trace/screenshot-artifakter for nem fejlanalyse.

Jonas Kirkeby har skrevet kode siden han som teenager forsøgte at lave en helt simpel hjemmeside til sin fars lille vvs-firma – og endte med at sidde oppe hele natten for at få en knap til at skifte farve. Siden da har han lært sig det meste ved at prøve sig frem, kopiere andres eksempler, ødelægge dem og langsomt forstå, hvorfor tingene virker, som de gør.

Til daglig arbejder han slet ikke med IT, men bruger aftener og morgener på små projekter: en lille side til en forening, et simpelt værktøj til at holde styr på familiens madplan eller et Python-script, der rydder op i rodede filer. Det er den slags konkrete hverdags-behov, der har formet hans måde at tænke kodning på – hvad kan jeg bygge nu, som faktisk hjælper mig eller nogen, jeg kender?

På Coding Class deler Jonas de guides, han selv ville ønske, han havde haft: korte, konkrete forløb, hvor du kan se noget på skærmen efter få minutters læsning. Han viser hele vejen fra idé til færdig løsning, inklusive de typiske fejl og små snubletråde på vejen, så du ikke kun får den pæne, polerede version.

Hans mål er, at du som begynder eller let øvet hurtigt får følelsen af: “Det her kan jeg faktisk selv finde ud af” – uanset om du vil bygge din første lille hjemmeside, forstå JavaScript-funktioner eller bruge Python til at automatisere en kedelig opgave.

Send kommentar

You May Have Missed