Att testa din kod
I A2 fick du 172 tester som talade om när en funktion var klar. I A3 får du inga. Det är medvetet: tester till en examinationsuppgift avslöjar mycket av designen, och i A3 är designen redan bestämd av klassdiagrammet. Det du ska visa är att du kan implementera efter det. A2 är faktiskt det enda stället i utbildningen där du får tester serverade. I kommande kurser, och i arbetslivet, skriver du dem själv.
Den här artikeln handlar om hur du gör det: först systematiskt för hand, sedan med kod som
kontrollerar sig själv och till sist med ett testramverk, Vitest. Den avslutas med en jämförelse med
två andra verktyg du kommer att stöta på, Jest och Node.js egen node:test.
Tidsåtgång: Ungefär 20 minuter.
Några korta frågor finns insprängda i texten. Försök svara innan du klickar fram lösningen.
Varför testa?
Section titled “Varför testa?”Ett test är ett litet program som kör en del av ditt program och kontrollerar att resultatet blir det du förväntar dig. Det ger dig tre saker:
- En körbar specifikation. För att skriva testet måste du bestämma vad som ska hända, innan du ser vad som händer. Det tvingar dig att läsa kraven noga.
- Ett skyddsnät. När du senare ändrar i
Deckför attCardTablebehöver något nytt, visar testerna direkt om något som fungerade tidigare har gått sönder. Ett sådant fel kallas en regression. - Tid. Ett manuellt test kostar lika mycket varje gång du gör det. Ett automatiskt test kostar en gång att skriva, och sedan nästan ingenting.
Tester kan visa att det finns fel, men aldrig att det inte finns några. Ett grönt testresultat betyder att de fall du har tänkt på fungerar, inte att programmet är korrekt.
Olika sorters tester
Section titled “Olika sorters tester”Tester delas ofta in efter hur stor del av programmet de omfattar:
| Typ | Vad testas | I A3 |
|---|---|---|
| Enhetstest (unit test) | En klass eller funktion för sig | Deck eller PlayingCard |
| Integrationstest | Flera delar tillsammans | CardTable med riktiga Deck- och Player-objekt |
| End-to-end-test (E2E) | Hela programmet, som en användare kör det | npm start 1 52, och kontroll av utskrift och statuskod |
En klassisk tumregel är testpyramiden: många snabba enhetstester i botten, färre integrationstester och bara ett fåtal end-to-end-tester i toppen. Ju högre upp, desto mer av programmet testas på en gång, men desto långsammare är testerna och desto svårare är det att se var felet sitter när ett test misslyckas.
Statisk analys, som ESLint, räknas ofta som pyramidens fundament. Den hittar fel utan att köra koden alls, och den har du redan.
Steg 1: manuellt, men systematiskt
Section titled “Steg 1: manuellt, men systematiskt”Det du gör när du skapar en kortlek i app.js och skriver ut den är ett manuellt test:
import { Deck } from './Deck.js'
const deck = new Deck()console.log(deck.toString())
deck.shuffle()console.log(deck.toString())Det fungerar, men bara om du vet vad du letar efter. Gör det till ett riktigt test genom att
bestämma innan du kör vad som ska skrivas ut: 52 kort, inga dubbletter, klöver först i ordningen
A, 2, 3 och så vidare. Efter shuffle() ska det vara samma 52 kort, men i en annan ordning. Skriv
ned fallen. Det är just den listan REFLECTION.md frågar efter.
Svagheten är att det är du som jämför. Du måste läsa utskriften med ögonen varje gång, och 52 kort är många att hålla reda på.
Steg 2: låt koden kontrollera sig själv
Section titled “Steg 2: låt koden kontrollera sig själv”Node.js har en inbyggd modul, node:assert, med funktioner som kastar ett undantag när något inte
stämmer. Med den kan koden göra jämförelsen åt dig:
import assert from 'node:assert/strict'import { PlayingCard } from './PlayingCard.js'
assert.equal(new PlayingCard(1, '♠').toString(), 'A♠')assert.equal(new PlayingCard(13, '♥').toString(), 'K♥')assert.equal(new PlayingCard(12, '♦').toString(), 'Q♦')
console.log('All checks passed.')Kör du filen med node src/check.js och allt stämmer skrivs den sista raden ut. Stämmer något
inte stannar programmet med ett AssertionError som visar vad som förväntades och vad som blev
resultatet.
Det här är ett stort steg framåt: du behöver inte längre läsa och jämföra själv. Men programmet stannar vid första fel, så du vet inte hur det gick för resten av kontrollerna, och när du har hundra kontroller i en fil blir det svårt att se vilken som hör till vad. Det är de problemen ett testramverk löser.
Steg 3: ett testramverk
Section titled “Steg 3: ett testramverk”Ett testramverk letar upp dina testfiler, kör varje test för sig, samlar resultaten och
presenterar dem. Den delen kallas på engelska test runner. Ramverket ger dig också funktioner för
kontrollerna, som expect(). Kursen använder Vitest, samma verktyg som i A2.
Installera Vitest
Section titled “Installera Vitest”A3-mallen innehåller inte Vitest, så du lägger till det själv:
npm install --save-dev vitest--save-dev gör Vitest till ett utvecklingsberoende (devDependency): något du behöver medan du
utvecklar, men som programmet inte behöver när det körs. Lägg sedan till två skript i
package.json:
"scripts": { "start": "node src/app.js", "test": "vitest run", "test:watch": "vitest", ...}npm test kör testerna en gång. npm run test:watch startar Vitest i bevakningsläge, så att
testerna körs om varje gång du sparar en fil.
Ett första test
Section titled “Ett första test”Testerna läggs i filer vars namn slutar på .test.js. Då hittar Vitest dem utan någon
konfiguration, var de än ligger i projektet. Var de ska ligga finns det två skolor:
- Bredvid filen som testas, som
src/PlayingCard.test.jsbredvidsrc/PlayingCard.js. Testet är lätt att hitta, importen blir kort (./PlayingCard.js), och flyttar du en modul följer testet med. Så är kursens övningsuppgifter i delkurs B uppbyggda, och så gör exemplen här. - I en egen katalog, som
test/PlayingCard.test.js, ofta med samma katalogstruktur somsrc. Programkoden och testerna hålls isär, och det är lätt att se vad som är vad. Mönstret är vanligt i äldre Node.js-projekt och i många andra språk, till exempel Java.
Båda fungerar, och det viktiga är att ett projekt håller sig till ett sätt.
import { describe, expect, test } from 'vitest'import { PlayingCard } from './PlayingCard.js'
describe('PlayingCard', () => { describe('toString()', () => { test('writes an ace as A', () => { const card = new PlayingCard(1, '♠')
const text = card.toString()
expect(text).toBe('A♠') })
test('writes jack, queen and king as J, Q and K', () => { expect(new PlayingCard(11, '♣').toString()).toBe('J♣') expect(new PlayingCard(12, '♦').toString()).toBe('Q♦') expect(new PlayingCard(13, '♥').toString()).toBe('K♥') }) })
test('cannot be changed after it has been created', () => { const card = new PlayingCard(1, '♠')
expect(() => { card.rank = 14 }).toThrow(TypeError) expect(card.rank).toBe(1) })})Tre funktioner bär det hela:
test(namn, funktion)definierar ett test. Namnet beskriver ett beteende, och funktionen innehåller testet. (itär ett alias förtest, och du ser båda i andras kod.)describe(namn, funktion)grupperar tester. Grupperna kan nästlas, och namnen bildar tillsammans en namnkedja,PlayingCard > toString() > writes an ace as A. Det är samma namnkedja som du filtrerade på i A2 mednpm run test:match.expect(värde)tar emot det faktiska värdet och ger tillbaka ett objekt med matchare som jämför det med det förväntade:toBe()för primitiva värden,toEqual()för innehållet i arrayer och objekt,toThrow()för undantag och många fler.
Det första testet är uppdelat i tre stycken med en tom rad emellan. Mönstret kallas Arrange–Act–Assert: förbered (skapa kortet), utför (anropa metoden) och kontrollera (jämför resultatet). Tester som följer det är lätta att läsa, även när de blir längre.
Kör testerna med npm test:
> assignment-a3-twenty-one@3.0.2 test> vitest run
RUN v5.0.5 /home/en999zz/kurser/1dv025/examination/assignment-a3
✓ src/PlayingCard.test.js (3 tests) 2ms ✓ PlayingCard (3) ✓ toString() (2) ✓ writes an ace as A 1ms ✓ writes jack, queen and king as J, Q and K 0ms ✓ cannot be changed after it has been created 0ms
Test Files 1 passed (1) Tests 3 passed (3) Start at 10:10:04 Duration 1.12s (import 54%, worker 23%, transform 22%, tests 2%)Vitest visar testerna som ett träd med samma struktur som describe-grupperna.
Fråga: varför blir det ett TypeError?
Section titled “Fråga: varför blir det ett TypeError?”PlayingCard anropar Object.freeze(this) sist i konstruktorn. Testet förväntar sig att
tilldelningen card.rank = 14 kastar ett TypeError. Men i ett vanligt skript skulle samma rad
inte kasta något fel alls. Vad skiljer, och varför kontrollerar testet dessutom card.rank
efteråt?
ES-moduler körs alltid i strict mode. I strict mode kastar en tilldelning till en egenskap i ett
fruset objekt ett TypeError. I ett vanligt skript utan 'use strict' ignoreras tilldelningen i
tysthet, och objektet är lika oförändrat, men inget fel syns.
Den andra kontrollen, att card.rank fortfarande är 1, testar det som egentligen är viktigt: att
kortet inte har ändrats. Undantaget är bara ett sätt som det märks på. Ett test som bara hade
kontrollerat undantaget hade gått igenom även om värdet ändrats och ett fel kastats av någon annan
anledning.
När ett test misslyckas
Section titled “När ett test misslyckas”Anta att du vill försäkra dig om hur ett ess och en kung räknas tillsammans. Enligt reglerna är esset värt 14 när det är mest fördelaktigt, så du skriver:
test('an ace and a king are worth 27', () => { const ace = new PlayingCard(1, '♠') const king = new PlayingCard(13, '♥')
expect(ace + king).toBe(27)}) FAIL src/ace.test.js > an ace and a king are worth 27AssertionError: expected 14 to be 27 // Object.is equality
- Expected+ Received
- 27+ 14
❯ src/ace.test.js:8:22 6| const king = new PlayingCard(13, '♥') 7| 8| expect(ace + king).toBe(27) | ^Vitest visar namnkedjan, det förväntade och det faktiska värdet, och raden där kontrollen gjordes.
Fråga: är det koden eller testet som är fel?
Section titled “Fråga: är det koden eller testet som är fel?”PlayingCard är färdig och får inte ändras. Är det då ett fel i klassen att ace + king blir 14?
Nej, det är testet som är fel. valueOf() returnerar kortets valör, och esset har valören 1.
Huruvida esset ska räknas som 1 eller 14 beror på vilka andra kort som finns på handen, och det kan
ett enskilt kort inte veta. Den regeln hör hemma hos den som räknar ut en hands värde, enligt
README-filen Players valueOf().
Ett test som misslyckas betyder att koden och förväntningen inte stämmer överens. Ibland är det förväntningen som är fel. Gå därför tillbaka till specifikationen, här README-filen och klassdiagrammet, innan du ändrar något.
Vad är värt att testa?
Section titled “Vad är värt att testa?”Du kan inte testa allt, och du behöver inte. Välj fall som har störst chans att avslöja ett fel:
- Normalfallet: det som oftast händer. En ny kortlek har 52 kort.
- Gränsfall: precis vid gränsen. Det sista kortet som delas ut, en hand med exakt fem kort, en
summa på exakt 21. Fel sitter ofta just vid gränserna: ett
<som borde ha varit<=. - Felfall: när något inte är som det ska. Ett ogiltigt argument ska ge ett undantag, och det
testar du med
toThrow().
Testa det publika gränssnittet
Section titled “Testa det publika gränssnittet”Deck lagrar sina kort i det privata fältet #playingCards, och det kan testet inte komma åt. Det
är en fördel, inte en begränsning. Testerna går via det som klassdiagrammet visar som publikt, här
toString():
import { describe, expect, test } from 'vitest'import { Deck } from './Deck.js'
/** * Returns the deck's cards as an array of strings, e.g. ['A♣', '2♣', ...]. * * @param {Deck} deck - The deck to read. * @returns {string[]} The cards in the deck's current order. */function cardsOf(deck) { return deck.toString().split(' ')}
describe('Deck', () => { test('a new deck has 52 different cards', () => { const cards = cardsOf(new Deck())
expect(cards).toHaveLength(52) expect(new Set(cards).size).toBe(52) })
describe('shuffle()', () => { test('keeps the same 52 cards', () => { const deck = new Deck() const before = cardsOf(deck)
deck.shuffle()
expect(cardsOf(deck).toSorted()).toEqual(before.toSorted()) })
test('changes the order of the cards', () => { const deck = new Deck() const before = cardsOf(deck)
deck.shuffle()
expect(cardsOf(deck)).not.toEqual(before) }) })})Tester som bara använder det publika gränssnittet fortsätter att fungera även om du byter ut hur klassen fungerar inuti. Det är samma tanke som i klassdiagrammet: det publika är ett kontrakt, och testerna kontrollerar att kontraktet hålls.
Hur testar du något som ska bli olika varje gång? Du kan inte förutsäga ordningen efter
shuffle(), men du kan testa egenskaper som alltid ska gälla: samma antal kort, samma kort, och
en annan ordning än före. Det är vad de två shuffle()-testerna gör. toSorted() ger en sorterad
kopia, så att de två kortlekarna kan jämföras oavsett ordning.
Fråga: kan testet “changes the order of the cards” misslyckas?
Section titled “Fråga: kan testet “changes the order of the cards” misslyckas?”Även om shuffle() är helt korrekt?
Ja, i teorin. shuffle() kan slumpa fram exakt samma ordning som från början. Det finns 52!, ungefär
8 · 1067, möjliga ordningar, så risken är försumbar.
Men principen är viktig. Ett test som ibland går igenom och ibland inte, utan att koden har ändrats, kallas ett flaky test, och det är en av de mest frustrerande sakerna i ett projekt. Med två kort i stället för 52 hade samma test misslyckats vartannat varv. Fundera alltid på om ett test som bygger på slump kan få fel utfall av en slump.
Testbar kod
Section titled “Testbar kod”Vissa delar av ett program är lätta att testa, andra svåra. Det som skiljer är oftast
sidoeffekter: utskrifter med console.log(), inläsning av process.argv och statuskoder med
process.exitCode. En metod som räknar ut ett värde och returnerar det kan testas med en rad. En
metod som skriver ut sitt resultat måste testet på något sätt fånga upp utskriften från.
Tumregeln är att hålla logiken och sidoeffekterna isär. Låt så mycket som möjligt av programmet ta
emot värden och returnera värden eller kasta undantag, och samla sidoeffekterna på ett ställe, i
app.js. Det är också därför den utmanande varianten av A3 låter playRounds() returnera
resultaten och säger att bara app.js får skriva ut något.
Det som ändå bara finns i app.js testar du end-to-end, för hand: kör npm start 1 52 och
kontrollera statuskoden med echo $? (Bash) eller $LastExitCode (PowerShell).
Samma test i andra verktyg
Section titled “Samma test i andra verktyg”Vitest är långt ifrån det enda verktyget. Här är ett av testerna ovan skrivet för Vitest, för Node.js inbyggda testramverk och för Jest.
import { describe, expect, test } from 'vitest'import { PlayingCard } from './PlayingCard.js'
describe('PlayingCard', () => { test('writes an ace as A', () => { const card = new PlayingCard(1, '♠')
expect(card.toString()).toBe('A♠') })
test('cannot be changed after it has been created', () => { const card = new PlayingCard(1, '♠')
expect(() => { card.rank = 14 }).toThrow(TypeError) })})npm install --save-dev vitestnpx vitest runimport { describe, test } from 'node:test'import assert from 'node:assert/strict'import { PlayingCard } from './PlayingCard.js'
describe('PlayingCard', () => { test('writes an ace as A', () => { const card = new PlayingCard(1, '♠')
assert.equal(card.toString(), 'A♠') })
test('cannot be changed after it has been created', () => { const card = new PlayingCard(1, '♠')
assert.throws(() => { card.rank = 14 }, TypeError) })})node --testimport { describe, expect, test } from '@jest/globals'import { PlayingCard } from './PlayingCard.js'
describe('PlayingCard', () => { test('writes an ace as A', () => { const card = new PlayingCard(1, '♠')
expect(card.toString()).toBe('A♠') })
test('cannot be changed after it has been created', () => { const card = new PlayingCard(1, '♠')
expect(() => { card.rank = 14 }).toThrow(TypeError) })})npm install --save-dev jest @jest/globalsnode --experimental-vm-modules node_modules/jest/bin/jest.jsBläddra mellan flikarna och lägg märke till hur lite som skiljer. describe och test finns i alla
tre. Vitest och Jest är identiska så när som på importraden, eftersom Vitest medvetet har kopierat
Jests API. node:test använder i stället node:assert för kontrollerna, samma modul som i steg 2.
| Vitest | node:test |
Jest | |
|---|---|---|---|
| Installation | npm-paket | Inbyggt i Node.js | npm-paket |
ES-moduler (import/export) |
Direkt | Direkt | Experimentellt, kräver en flagga till Node.js |
| Kontroller | expect(...).toBe(...) |
assert.equal(...) |
expect(...).toBe(...) |
| Bevakningsläge | vitest |
node --test --watch |
jest --watch |
| Används mest till | Nya projekt, allt som byggs med Vite | Mindre bibliotek och verktyg utan beroenden | Befintliga, ofta äldre, kodbaser |
Jests ESM-stöd är det som märks mest i en kurs där all kod är ES-moduler. Utan flaggan
--experimental-vm-modules vägrar Jest 30 att köra testfilen ovan, och med den skriver Node.js ut en
varning om att funktionen är experimentell.
Det finns fler verktyg än dessa tre. Mocha, ofta tillsammans med kontrollbiblioteket Chai, var länge standard i Node.js-världen och finns kvar i många projekt. Körmiljöerna Deno och Bun har egna inbyggda testramverk.
Vad använder branschen?
Section titled “Vad använder branschen?”Vitest har på kort tid tagit över efter Jest som förstahandsval i nya projekt. Några siffror från hösten 2026:
- Under en vecka i oktober 2026 laddades Vitest ned omkring 78 miljoner gånger från npm, Jest omkring 26 miljoner och Mocha 7 miljoner. Ett år tidigare var förhållandet det omvända: Jest omkring 32 miljoner, Vitest 16 miljoner.
- I enkätundersökningen State of JavaScript 2025 ökade Vitest mest av alla verktyg i andel användare.
- Ramverket Angular bytte till Vitest som standard i version 21, i november 2025. Vue, Nuxt och SvelteKit använder det redan.
Nedladdningssiffror mäter installationer, mest från automatiska byggen, och inte antalet utvecklare. Men trenden är tydlig. Jest finns samtidigt kvar i ett mycket stort antal befintliga kodbaser, och eftersom Vitest använder samma API kan du läsa och skriva Jest-tester med det du lär dig här.
Kursen använder Vitest av tre skäl: det hanterar ES-moduler direkt, det bygger på Vite som du använder i delkurs B, och det har samma API som Jest.
Tillbaka till A3
Section titled “Tillbaka till A3”REFLECTION.md frågar vilka fall du har testat och hur, och vad du hade gjort annorlunda med
automatiska tester. Med den här artikeln har du ett språk för båda frågorna: vilka fall som är
normalfall, gränsfall och felfall, vad som testas som enhet och vad som testas end-to-end, och
vilka delar av din kod som är lätta respektive svåra att testa.
Automatiska tester ligger utanför kursen, men är du nyfiken kan du prova på dina egna klasser. Börja
i så fall där du redan började bygga, i Deck. Vad ska count vara för en ny kortlek, och efter ett
deal()? Vad ska hända när kortleken är tom? Även om du aldrig skriver testerna är frågorna samma
som de du ställer dig när du testar för hand.
Att ta med sig
Section titled “Att ta med sig”- Ett test kontrollerar att resultatet blir det du bestämt i förväg. Bestäm förväntningen innan du kör.
- Enhetstester testar en del för sig, end-to-end-tester hela programmet. Många små, få stora.
describegrupperar,testdefinierar ochexpectkontrollerar. Följ Arrange–Act–Assert.- Testa normalfall, gränsfall och felfall, via det publika gränssnittet. Testa egenskaper när resultatet bygger på slump.
- Håll logik och sidoeffekter isär, så blir koden lätt att testa.
- Vitest, Jest och
node:testdelar samma grundidéer. Kan du ett av dem kan du läsa de andra.
Vidare läsning
Section titled “Vidare läsning”- Getting Started och expect i Vitests dokumentation.
- Test runner och Assert i Node.js dokumentation.
- The Practical Test Pyramid av Ham Vocke på Martin Fowlers webbplats.
- Guiden om att köra bara vissa tester, som även visar Vitest-tillägget till Visual Studio Code.