Test uden tårer i Vite-projekter

Test uden tårer i Vite-projekter

Start med én test, ikke et test-framework

Installér Vitest, skriv én lille test til én lille funktion, og kør den, før du overhovedet overvejer mapper, coverage og alle de fine ord.

Da jeg første gang satte Vitest op i et Vite-projekt, var det til en helt banal to-do app. Jeg var træt af, at noget gik i stykker hver gang jeg “lige” ryddede lidt op i koden aftenen før sengetid. Næste dag var halvdelen af tingene døde, og jeg kunne ikke huske, hvad jeg havde rørt.

Her er den version, som til sidst virkede for mig, uden jeg druknede i config-filer.

Hvad en unit test faktisk er i din lille app

Jeg starter ikke med definitioner, jeg starter med en funktion jeg faktisk brugte.

I min to-do app havde jeg en lille hjælperfunktion til at tilføje en opgave:

// src/lib/todos.js
export function addTodo(todos, text) {
  const trimmed = text.trim()
  if (!trimmed) return todos

  return [
    ...todos,
    { id: crypto.randomUUID(), text: trimmed, completed: false }
  ]
}

En unit test her er bare et lille stykke kode som tjekker, at funktionen gør det, jeg forventer. Ikke mere magisk end at åbne devtools og prøve noget af i konsollen, men gentageligt.

Min første test til den funktion så sådan her ud:

// src/lib/todos.test.js
import { describe, it, expect } from 'vitest'
import { addTodo } from './todos'

describe('addTodo', () => {
  it('tilføjer en ny todo med trimmed tekst', () => {
    const todos = []

    const result = addTodo(todos, '  Køb mælk  ')

    expect(result).toHaveLength(1)
    expect(result[0].text).toBe('Køb mælk')
    expect(result[0].completed).toBe(false)
  })
})

Det er en unit test:

Jeg giver funktionen noget input, kører den, og tjekker resultatet automatisk.

Ingen browser, ingen klik rundt. Bare “hvis input er sådan her, skal output være sådan her”.

Hvis du først lige får følelsen af at “nå, det er bare det”, bliver resten af Vitest meget mindre skræmmende.

Vitest i et Vite-projekt på 10 minutter

Jeg tager udgangspunkt i et frisk Vite projekt med JavaScript. Hvis du allerede har et projekt, kan du hoppe til installationen.

Først opretter jeg projektet:

npm create vite@latest my-vitest-app -- --template vanilla
cd my-vitest-app
npm install

Så kommer Vitest på:

npm install -D vitest

Vite har allerede en fornuftig standardopsætning til test indbygget. Jeg tilføjer bare et lille test-script i package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest"
  }
}

Vitest finder som standard filer der hedder noget med .test.js eller .test.ts. Så jeg laver den simpleste tænkelige test i src/example.test.js:

import { it, expect } from 'vitest'

it('1 + 1 er 2', () => {
  expect(1 + 1).toBe(2)
})

Nu kan jeg køre:

npm test

Hvis du ser en grøn test her, er du faktisk allerede “i gang med Vitest”. Resten handler mere om, hvad du vælger at teste.

Tre typer tests du skal skrive først

I mine små projekter ender jeg næsten altid med samme mønster: en ren funktion, et edge case og en fejl-case. Sådan så det ud i min to-do app.

1. Ren funktion: det der skal virke på en god dag

Den første test har jeg allerede vist: den glade vej, hvor brugeren skriver noget fornuftigt ind, og alt går godt.

Hvis vi tager addTodo som eksempel, er det den her:

it('tilføjer en ny todo med trimmed tekst', () => {
  const todos = []

  const result = addTodo(todos, '  Køb mælk  ')

  expect(result).toHaveLength(1)
  expect(result[0].text).toBe('Køb mælk')
})

Den test fanger de klassiske “jeg ændrede lige et felt-navn” eller “jeg glemte at returnere arrayet”.

2. Edge case: det brugeren ikke burde gøre, men gør alligevel

Her er det oftest tomme værdier, null, undefined eller alt for lange strenge.

I min app ville jeg ikke gemme tomme to-dos, selv hvis brugeren bare trykkede enter på et tomt input. Så kom test nummer to:

it('ignorerer tom tekst', () => {
  const todos = [{ id: '1', text: 'Eksisterende', completed: false }]

  const result = addTodo(todos, '   ')

  expect(result).toEqual(todos)
})

Nu kan jeg roligt refaktorere funktionen, uden at være bange for at begynde at få tomme opgaver ind.

3. Fejl-case: det der ikke må ske uden en klar reaktion

Nogle gange vil du hellere kaste en fejl end at forsøge at gætte dig til, hvad der skulle være sket.

Lad os sige, at addTodo kun skal have et array som første argument. Hvis jeg pludselig kalder den forkert, vil jeg gerne have en tydelig fejl. Sådan her:

export function addTodo(todos, text) {
  if (!Array.isArray(todos)) {
    throw new TypeError('todos skal være et array')
  }
  const trimmed = text.trim()
  if (!trimmed) return todos

  return [
    ...todos,
    { id: crypto.randomUUID(), text: trimmed, completed: false }
  ]
}

Og testen:

it('kaster fejl hvis første argument ikke er et array', () => {
  expect(() => addTodo(null, 'Hej')).toThrowError('todos skal være et array')
})

De tre tests tilsammen gør, at jeg roligt kan rydde op i funktionen uden at skulle klikke rundt i UI’et hver gang.

Mock fetch i Vitest uden at ende i kaos

Det næste skridt som plejer at gå galt i små projekter er API-kald. Specielt når man leger med fetch i en Vite app.

Jeg havde en lille helper til at hente to-dos fra et API:

// src/lib/api.js
export async function fetchTodos() {
  const res = await fetch('https://example.com/api/todos')

  if (!res.ok) {
    throw new Error('Kunne ikke hente todos')
  }

  return res.json()
}

Den vil jeg gerne teste, uden at kalde et rigtigt API. Så jeg mocker fetch.

Mock succes-svar

Vitest kører i Node, men har som standard et jsdom miljø, så globalThis.fetch findes typisk ikke. Jeg plejer bare at overskrive det på testeniveau.

// src/lib/api.test.js
import { describe, it, expect, vi } from 'vitest'
import { fetchTodos } from './api'

describe('fetchTodos', () => {
  it('returnerer todos ved succes', async () => {
    const fakeTodos = [
      { id: '1', text: 'Test', completed: false }
    ]

    // mock fetch
    globalThis.fetch = vi.fn(async () => ({
      ok: true,
      json: async () => fakeTodos
    }))

    const result = await fetchTodos()

    expect(fetch).toHaveBeenCalledOnce()
    expect(result).toEqual(fakeTodos)
  })
})

Her er pointen: jeg tester min fetchTodos funktion, ikke API’et. Jeg styrer selv, hvad fetch “svarer”.

Mock fejl-svar

Min anden test er altid fejlvejen. Hvad sker der, hvis API’et svarer med en 500 eller 404?

it('kaster fejl hvis svaret ikke er ok', async () => {
  globalThis.fetch = vi.fn(async () => ({
    ok: false,
    json: async () => ({ message: 'Fejl' })
  }))

  await expect(fetchTodos()).rejects.toThrowError('Kunne ikke hente todos')
})

To tests. En for succes, en for fejl. Så er den helper nogenlunde sikret mod de klassiske “jeg glemte lige at tjekke res.ok” eller “jeg ændrede URL’en og brød noget”.

Hvis du er nysgerrig på fetch generelt, har jeg tidligere haft glæde af at læse om forskelle på fetch og Axios, fx i artikler a la Axios vs fetch. Men til tests er fetch plus en god mock ofte fint til små projekter.

Test af localStorage uden at svine test-miljøet til

Det næste sted hvor mine små apps ofte opførte sig mærkeligt, var når jeg gemte ting i localStorage. Specielt når flere tests påvirkede hinanden.

Jeg havde to simple funktioner:

// src/lib/storage.js
const KEY = 'todos'

export function saveTodos(todos) {
  localStorage.setItem(KEY, JSON.stringify(todos))
}

export function loadTodos() {
  const raw = localStorage.getItem(KEY)
  if (!raw) return []
  try {
    return JSON.parse(raw)
  } catch (e) {
    return []
  }
}

Her bruger jeg også en mock, så jeg ikke er afhængig af rigtigt localStorage. Vitest har dog allerede et jsdom miljø med en browser-lignende localStorage, så du kan faktisk bruge det direkte.

Testene kunne se sådan her ud:

// src/lib/storage.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { saveTodos, loadTodos } from './storage'

describe('storage helpers', () => {
  beforeEach(() => {
    localStorage.clear()
  })

  it('gemmer og loader todos', () => {
    const todos = [{ id: '1', text: 'Gem mig', completed: false }]

    saveTodos(todos)
    const loaded = loadTodos()

    expect(loaded).toEqual(todos)
  })

  it('returnerer tomt array hvis ingenting er gemt', () => {
    const loaded = loadTodos()
    expect(loaded).toEqual([])
  })

  it('returnerer tomt array ved ødelagt JSON', () => {
    localStorage.setItem('todos', '{ ikke gyldig JSON ')
    const loaded = loadTodos()
    expect(loaded).toEqual([])
  })
})

Tricket er beforeEach, som rydder op, før hver test kører. Det er nok den mest oversete ting af begyndere: tests må ikke være afhængige af, hvad andre tests har lavet.

Hvis du er i tvivl om jsdom og miljøer i Vite, kan det være værd at kigge forbi andre artikler på Coding Class, hvor miljø-opsætning og tooling tit bliver taget mere grundigt.

Hvornår jeg ikke gider unit-teste (og hvad jeg gør i stedet)

Nogle ting tester jeg ærligt talt ikke med Vitest. Ikke fordi det er “forkert”, men fordi tiden er bedre brugt et andet sted.

Jeg skriver typisk ikke unit tests til:

Meget simpel styling og CSS. Om en knap er blå eller grøn, fanger jeg hurtigere med øjnene.

Ren rendering af statisk HTML. Hvis det bare er en tekstblok der vises, er jeg tilfreds med at se den i browseren.

Midlertidig UI-logik i små eksperimenter. Hvis jeg ved, at noget er et engangseksperiment, skriver jeg sjældent tests, før jeg kan mærke, at det faktisk bliver til noget.

I de tilfælde bruger jeg i stedet en lille tjek-rutine når jeg klikker rundt i browseren: åbn siden, test de 3 vigtigste flows manuelt (fx “tilføj todo”, “markér todo som færdig”, “reload siden og se om de er der”) og gør det hver gang jeg har lavet en større ændring.

Hvis jeg mærker, at jeg bliver træt af at gentage det, er det faktisk et fint signal til, at “ok, nu er der noget her, der skal have en test”.

En minimal test-strategi til dine portfolio-projekter

Hvis du bare vil have et fornuftigt bundniveau i dine Vite-projekter uden at lave en hel test-afdeling, kan du nøjes med meget lidt.

Jeg plejer at sigte efter cirka fem tests i en lille to-do eller notes-app.

To tests til dine vigtigste data-funktioner. For eksempel en “addTodo” og en “toggleTodoCompleted”. Én for den glade vej, én for en edge case.

To tests til dine side-effekter. Typisk én til fetch og én til localStorage, som vist ovenfor.

Én test til en kritisk validering. For eksempel en funktion der tjekker om inputfeltet har mindst 3 tegn, før du sender det til API’et.

Et eksempel på sådan en valideringsfunktion:

// src/lib/validation.js
export function isValidTodoText(text) {
  if (typeof text !== 'string') return false
  const trimmed = text.trim()
  return trimmed.length >= 3
}

Og en lille testfil:

// src/lib/validation.test.js
import { describe, it, expect } from 'vitest'
import { isValidTodoText } from './validation'

describe('isValidTodoText', () => {
  it('godkender tekst med mindst 3 tegn', () => {
    expect(isValidTodoText('Hej')).toBe(true)
  })

  it('afviser korte eller tomme tekster', () => {
    expect(isValidTodoText('  ')).toBe(false)
    expect(isValidTodoText('ok')).toBe(false)
    expect(isValidTodoText(' x ')).toBe(false)
  })

  it('afviser ikke-strenge', () => {
    expect(isValidTodoText(null)).toBe(false)
    expect(isValidTodoText(123)).toBe(false)
  })
})

Fem tests. Ikke perfekt dækning, men nok til at du roligt kan vise projektet på GitHub uden at være bange for, at en lille ændring vælter alt.

Hvis du vil have mere inspiration til portfolio-struktur, har jeg tidligere skrevet om at gøre dine projekter mere interessante end bare et link til GitHub. Tests er et af de steder, hvor du ret hurtigt kan skille dig lidt ud.

Typiske Vitest-faldgruber når du er ny

Jeg løb selv ind i de samme tre problemer igen og igen, indtil jeg fik dem banket på plads.

ESM vs CommonJS

Vite og Vitest forventer moderne ES modules. Så brug import og export i stedet for require og module.exports.

Hvis du ser fejl a la “Cannot use import statement outside a module”, så tjek at:

Dine filer har .js eller .mjs-endelse, ikke blandet CommonJS ind fra gamle tutorials.

Din package.json enten har "type": "module", eller at du følger Vites standardstruktur.

Miljø og globale objekter

Når du bruger ting som window, document, localStorage eller fetch, så husk at tests kører uden for din rigtige browser.

Vitest bruger typisk jsdom, så en del findes, men jeg plejer at være eksplicit:

Når jeg tester rene helpers, lader jeg være med at bruge DOM overhovedet. Så er der færre ting at slås med.

Når jeg tester noget der rører DOM eller globale objekter, mocker jeg det selv, som jeg gjorde med fetch og localStorage.

Flakey tests: når testen nogle gange fejler, nogle gange ikke

Min erfaring er, at flakey tests næsten altid handler om én af to ting:

Du deler state mellem tests. For eksempel et modul hvor en variabel ændres og ikke nulstilles mellem tests.

Du glemmer at vente på async ting. Altså glemmer await eller bruger callbacks på en måde, der ikke spiller sammen med Vitest.

Jeg løser det typisk ved:

At bruge beforeEach til at nulstille ting som localStorage, mock-states og lignende.

At gøre alle tests der rører async funktioner til async tests, og bruge await, som du så i mine fetch-eksempler.

Når noget stadig driller, starter jeg med kun at køre den ene testfil:

npx vitest src/lib/api.test.js

Og ja, jeg har også siddet en sen aften og bandet over en test, der kun fejlede hver tredje gang. Den slags husker man, og så bliver man pludselig mere omhyggelig med sin oprydning.

Det ene råd du skal tage med

Start med at teste dine rene funktioner i Vite-projektet med Vitest, før du begynder at rode med UI og DOM, for de tests er nemmest at skrive og giver mest ro i maven for mindst arbejde.

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.

1 kommentar

comments user
Torben

altså min datter bad mig hjælpe hendes søn med en skoleside, jeg læste om én test 😂 det gav mening

Send kommentar

You May Have Missed