Sådan bliver du bedre til test med testdrevet udvikling

Testning er en vigtig færdighed, som enhver udvikler skal have. Stadig er nogle udviklere tilbageholdende med at teste.

Vi har alle på et tidspunkt mødt en udvikler, der har sagt noget i retning af "tests er ubrugelige", "Det kræver for meget indsats" eller "Jeg tvivler ikke på min kode. Hvorfor spilde min tid på test? ”. Lyt ikke til dem. Testning er vigtig.

En stor grund er, at test gør din kode mere stabil og reducerer dine chancer for at få fejl. Du kan måske tro, at dette ikke er sandt, fordi du kender enhver lille smule af din kode. Jeg mener, du byggede det, så hvorfor skulle du skrive prøver på ting, du allerede ved?

Lad os antage, at du bygger en vejr-app. Du har kodet i et par dage eller et par uger, så du mestrer din kode.

Lad os nu antage, at du holder op med at bygge denne app, og du kommer tilbage til den få måneder senere. Du kan ikke huske enhver detalje i din gamle kode. Du skal ændre det, og helvede, noget brød. Hvordan løser du det? Ved at kigge på hver fil, du har oprettet og finjustere dem for at få den til at fungere igen? Det fungerer muligvis. Men geez, ved at ændre denne fil, brød du noget andet.

Derefter kan du tænke ”Uanset hvad, jeg vidste ikke, hvordan man kode. Jeg forlader bare denne app uændret, og jeg går videre til noget andet. ”

Lad os tage et andet eksempel. Efter måneder med hårdt arbejde landede du endelig det udviklerjob, du altid har ønsket! Du integreres i et team, og du begynder at bygge noget. Du arbejder på andres kode og vice versa. Og tingene går i stykker. Hvis holdet ikke integrerede test i deres app, ønsker jeg dig held og lykke med at fejlsøge den.

Enhver teknologisk virksomhed skal skrive test, når de bygger software eller apps. Så du ønsker ikke at være den person, der ikke ved, hvordan man tester og kæmper med at skrive prøver i de første uger.

Så ja, det tager tid at skrive prøver. Ja, det er svært i starten. Ja, det er mere interessant at opbygge appen. Men test er vigtige og sparer tid, når de er korrekt implementeret.

Dette er mit mål i dag: at forbedre dine testfærdigheder. Vi vil opdage enhedstesting og testdrevet udvikling med Jest (et JavaScript-testværktøj) ved at oprette en stak og teste den.

Der er selvfølgelig andre testværktøjer, som du kan bruge som Mokka og Chai. Men vi kan bruge Jest lige ud af boksen. Det er hurtigt, og alt er indbygget: påståelsesbibliotek, mock, snapshot-test. Så lad os komme i gang!

Enhedstest

Når du beslutter at teste en app, støder du på forskellige typer test: enhedsprøvning, integrationstest og funktionelle test. Vi vil fokusere på enhedstest i denne tutorial. Selv om funktionelle og integrationstests også er vigtige, er de sværere at installere og implementere end enhedsprøver. Desuden får du allerede en masse værdi af enhedstest.

Kort sagt består enhedstest af at teste små dele af din kode: funktioner, klassemetoder osv. Du giver dem input og du kontrollerer, at du får den forventede output.

Her er fordelene ved enhedstest:

  • Det gør din kode mere stabil.
  • Det er lettere at ændre den tekniske implementering af en funktion uden at ændre dens opførsel.
  • Det dokumenterer din kode. Hvorfor snart?
  • Det tvinger dig til at have et fantastisk kodedesign. Faktisk er dårligt designet kode ofte sværere at teste.

Testdrevet udvikling (TDD)

For at forstå og bruge testdrevet udvikling skal du bare anvende disse to regler:

  • Skriv en test, der mislykkes, før du selv skriver koden.
  • Skriv derefter ikke mere kode end det er tilstrækkeligt til at bestå den mislykkede test.

Når vi bruger TDD, taler vi også om Rød / Grøn / Refaktor-cyklus.

  • Rødt: du skriver en mislykket test uden at skrive koden.
  • Grøn: skriv den enkleste kode for at få testen bestået. Selv hvis koden forekommer dum eller enkel.
  • Refactor: Refactor den kode, du har skrevet om nødvendigt. Faktisk sørgede du for, at det, du testede, har den rigtige opførsel. Ingen grund til at bekymre dig, hvis du refaktorerer koden, dine enhedsprøver går i stykker, hvis noget går galt.

Lyder teoretisk? Bare rolig. Du forstår ved at øve dig.

Strukturering af en testfil

Jest leverer funktioner til strukturering af dine test:

  • beskriv: bruges til at gruppere dine prøver og beskrive opførsel af din funktion / modul / klasse. Det tager to parametre. Den første er en streng, der beskriver din gruppe. Den anden er en tilbagekaldsfunktion, hvor du har dine testsager eller krogfunktioner.
  • it eller test: din test case, det vil sige din unit test. Parametrene er nøjagtigt de samme som beskrevet. Det skal være beskrivende. Navngivningen af ​​din test er op til dig, men det er temmelig konventionelt at begynde dem med "Bør".
  • beforeAll (afterAll): krogfunktion, der kører før (og efter) alle test. Det tager en parameter, som er den funktion, du vil køre før (og efter) alle test.
  • beforeEach (afterEach): krogfunktion, der kører før (og efter) hver test. Det tager en parameter, som er den funktion, du vil køre før (og efter) hver test.

Du skal også vide følgende, før du skriver en test:

  • Du kan springe din test over ved hjælp af .skip på beskriv og den: it.skip (...) eller beskriv.skip (...). Ved at bruge .skip fortæller du Jest at ignorere testen eller gruppen.
  • Du kan vælge nøjagtigt hvilke test, du vil køre ved at bruge .only på beskriv og det: it.only (...) eller beskriv.only (...). Det er nyttigt, hvis du har en masse test og kun vil fokusere på en test, eller hvis du vil "fejlsøge" dine test.

Opsætning af Jest

For at vise dig testfunktionerne, vi brugte ovenfor, er vi nødt til at konfigurere Jest. Bare rolig, det vil være dødt enkelt.

Som forudsætninger har du kun brug for Node.js og npm eller Garn. Sørg for, at du bruger den nyeste version af Node.js, da vi vil bruge ES6. Opret en ny mappe, og initialiser den.

mkdir testeksempel && cd testeksempel
npm start -y
# ELLER
garn init -y

Den svarer ja på alle spørgsmål om npm eller garn. Det burde have genereret en meget grundlæggende package.json-fil.

Tilføj derefter Jest i dine udviklerafhængigheder:

garn tilføj jest - dev

Til sidst tilføj følgende script til din package.json:

"scripts": {
  "test": "jest"
}

garntest kører dine testfiler i dit bibliotek. Som standard genkender Jest filerne, der er inde i et bibliotek kaldet __tests__ eller filerne, der slutter enten med .spec.js eller .test.js.

Og det er alt. Du er klar til at skrive dine første test.

matchers

Når du tester noget, har du brug for et input og en forventet output. Derfor tilbyder Jest matchere til at teste vores værdier:

forventer (input) .matcher (output)

Jest har mange matchere, så her er de mest almindelige:

  • toBe: sammenligner streng lighed (===).
forvente (1 + 1) .toBe (2)
lad testsAreEssential = sandt
forventer (testAreEssential) .toBe (sand)
  • toEqual: sammenligner værdier mellem to variabler eller arrays eller objekter.
lad arr = [1, 2]
arr.push (3)
forvente (arr) .toEqual ([1, 2, 3])
lad x = 1
x ++
forvente (x) .toEqual (2)
  • toBeTruthy (toBeFalsy): fortæller, om værdien er sand (falsk).
forventer (null) .toBeFalsy ()
forventer (undefined) .toBeFalsy ()
forventer (falsk) .toBeFalsy ()
forventer ("Hej verden"). toBeTruthy ()
forventer ({foo: 'bar'}). toBeTruthy ()
  • ikke: skal placeres foran en matcher og returnerer det modsatte af matcherens resultat.
forventer (null) .not.toBeTruthy ()
// samme som forvent (null) .toBeFalsy ()
forventer ([1]). not.toEqual ([2])
  • toContain: kontrollerer, om arrayet indeholder elementet i parameteren.
forventer (['Apple', 'Banana', 'Strawberry']). toContain ('Apple')
  • toThrow: kontrollerer, om en funktion kaster en fejl
funktion tilslut () {
  smid nye ConnectionError ()
}
forventer (forbinde) .toThrow (ConnectionError)

Disse er ikke de eneste matchere. Du kan finde alle Jest-matchere her.

Første test

Nu skal vi skrive vores første test og lege med vores funktioner. Opret først i dit bibliotek en fil kaldet eksempel.spec.js og indsæt følgende indhold:

beskriv ('Eksempel', () => {
  beforeAll (() => {
    console.log ('kører inden alle test')
  })
  afterAll (() => {
    console.log ('kører efter alle test')
  })
  beforeEach (() => {
    console.log ('kører før hver test')
  })
  afterEach (() => {
    console.log ('kører efter hver test')
  })
  it ('Bør gøre noget', () => {
    console.log ('første test')
  })
  it ('Bør gøre noget andet', () => {
    console.log ('anden test')
  })
})

Bemærk, at vi ikke behøver at importere alle de funktioner, vi bruger. De er allerede leveret af Jest.

Kør garntest:

Undersøgelse af forskellige testfunktioner

Da du ikke har påstande i dine test, vil de bare bestå. Har du set de forskellige console.log-udsagn? Du skal forstå bedre, hvordan din krog fungerer og testkasser fungerer nu.

Fjern nu alle krogfunktioner og tilføj et .skip på den første test:

beskriv ('Eksempel', () => {
  it.skip ('Bør gøre noget', () => {
    console.log ('første test')
  })
  it ('Bør gøre noget andet', () => {
    console.log ('anden test')
  })
})

Kør garntest igen:

Det er logisk, siden du sprang over en test. Den første løber bare ikke.

Føj nu en tredje test til din testsuite og brug .only på den:

beskriv ('Eksempel', () => {
  it ('Bør gøre noget', () => {
    console.log ('første test')
  })
  it ('Bør gøre noget andet', () => {
    console.log ('anden test')
  })
  it.only ('Bør gøre det', () => {
    console.log ('tredje test')
  })
})

Kør garntest endnu en gang:

Igen, logisk. Du beder Jest om kun at køre din tredje test. Så du ser kun tredje test i konsollen.

Testning af en stak med testdrevet udvikling

Ikke mere teori. Tid til at øve.

I det følgende foretager vi en enkel implementering af en stak i JavaScript med testdrevet udvikling.

Som en påmindelse er en stak en datastruktur, mere præcist en LIFO-struktur: Last In, First Out. Der er tre hovedoperationer, der skal udføres på en stak:

  • push: skubber et element øverst i en stak.
  • pop: fjerner elementet øverst i en stak.
  • kig: returnerer det sidste element øverst i en stak.

I vores tilfælde opretter vi en klasse, hvis navn er Stak. For at gøre tingene mere komplekse antager vi, at denne stak har en fast kapacitet. Her er egenskaberne og funktionerne i vores stack-implementering:

  • emner: stakens genstande. Vi bruger en matrix til at implementere stakken.
  • kapacitet: stablets kapacitet.
  • isEmpty (): returneres true, hvis stakken er tom, ellers falsk.
  • isFull (): returnerer sandt, hvis stakken har nået sin maksimale kapacitet, det vil sige når du ikke kan skubbe til et andet element. Returnerer falsk ellers.
  • push (element): skubber et element på stakken. Returnerer fuld, hvis stakken er fuld, skubbes elementet på anden måde.
  • pop (): fjerner det sidste element i stakken. Returnerer Tom, hvis stakken er tom, hvis elementet poppes ellers.
  • kig (): returnerer elementet øverst i en stak (den sidste skubbes derefter). Returnerer Tom, hvis stakken er tom, returnerer elementet ellers.

Vi vil oprette to filer stack.js og stack.spec.js. Jeg brugte .spec.js-udvidelsen, fordi jeg er vant til den, men du er fri til at bruge .test.js eller give den et andet navn og placere den under __tests__.

Når vi laver testdrevet udvikling, så lad os skrive den mislykkede test. Vi tester konstruktøren først. For at teste din fil skal du importere din stakefil:

const Stack = kræver ('./ stack')

For dem, der spekulerer på, hvorfor jeg ikke har brugt import her, skyldes det, at den seneste stabile version af Node.js ikke understøtter den fra i dag. Jeg kunne have tilføjet Babel, men jeg vil ikke overbelaste denne tutorial. Så lad os holde os med kræve.

En god ting at gøre, når du tester en klasse eller en funktion er at starte din test ved at beskrive hvilken fil eller klasse du tester. Her handler det om en stak:

beskriv ('Stak', () => {
})

Derefter skal vi teste, at når vi initialiserer en stabel, opretter vi en tom matrix, og vi indstiller den korrekte kapacitet. Så i beskrivelsesblokken skriver vi følgende test:

it ('Bør konstruere stakken med en given kapacitet', () => {
  lad stak = ny stak (3)
  forventer (stack.items) .toEqual ([])
  forventer (stack.capacity) .toBe (3)
})

Bemærk, at vi bruger toEqual og ikke toBe til stack.items, fordi de ikke refererer til den samme matrix. Så vi er kun nødt til at sammenligne deres værdier.

Kør nu garntest stack.spec.js. Vi kører Jest på en bestemt fil, fordi vi ikke ønsker at blive forurenet af de andre test. Her er resultatet:

Stakken er ikke en konstruktør. Selvfølgelig. Vi har stadig ikke oprettet vores klasse Stack og leverede den en konstruktør.

Opret din klasse, en konstruktør i stack.js og eksporter klassen:

klasse stak {
  konstruktør () {
  }
}
module.exports = Stak

Kør testen igen:

Da vi ikke har indstillet emner i konstruktøren, forventede Jest, at emnerne i matrixen ville være lig [], men det blev udefineret. Du skal initialisere elementerne derefter:

konstruktør () {
  this.items = []
}

Hvis du kører testen igen, får du den samme type fejl for kapacitet, så du bliver nødt til at indstille kapaciteten også:

konstruktør (kapacitet) {
  this.items = []
  dette.kapacitet = kapacitet
}

Kør vores test:

Yeah! PASSERE. Så du, hvordan vi skrev løsningen? Det er, hvad TDD handler om. Den dækker din kode til enhver tid og lader dig komme langsomt hen imod løsningen, når du løser de mislykkede test. Jeg håber, at test er mere fornuftigt for dig nu! Så lad os fortsætte, skal vi?

er tom

For at teste isEmpty vil vi initialisere en tom stabel, teste om isEmpty returnerer sandt, tilføje et element og test det igen.

it ('Bør have en isEmpty-funktion, der returnerer sand, hvis stakken ellers er tom og falsk', () => {
  lad stak = ny stak (3)
  forventer (stack.isEmpty ()). Tobe (sand)
  stack.items.push (2)
  forventer (stack.isEmpty ()). Tobe (falsk)
})

Hvis du kører din test, skal du få følgende fejl:

TypeError: stack.is Tom er ikke en funktion

For at løse dette problem bliver vi nødt til at oprette is tom i Stack-klassen:

er tom () {
}

Hvis du kører testen, skal du få en anden fejl:

Forventet: sandt
Modtaget: udefineret

Giver mening. Intet er tilføjet inde i er tom. En stak er tom, hvis der ikke er nogen genstande i den:

er tom () {
  return this.items.length === 0
}

er fuld

Dette er nøjagtig det samme som isTom, så som en øvelse, test denne funktion ved hjælp af testdrevet udvikling. Du finder løsningen i bunden af ​​denne tutorial.

Skubbe

Vi er nødt til at teste tre forskellige ting her:

  • et nyt element skal tilføjes oven på stakken.
  • push returnerer "Fuld", hvis stakken er fuld.
  • elementet, der for nylig blev skubbet, skal returneres.

Vi vil oprette en anden blok ved hjælp af beskriv til push. Rede denne blok inde i din vigtigste beskrivelsesblok.

beskriv ('Stack.push', () => {
  
})

Tilføjelse af et element

For at teste den, opretter vi en ny stak og skubber et element. Det sidste element i artiklen skal være det element, du lige har tilføjet.

beskriv ('Stack.push', () => {
  it ('Bør tilføje et nyt element øverst på stakken', () => {
    lad stak = ny stak (3)
    stack.push (2)
    forvente (stack.items [stack.items.length - 1]). toBe (2)
  })
})

Hvis du kører testen, kan du se, at push ikke er defineret, hvilket er en gang til, helt normalt. push har brug for en parameter for at skubbe noget ind på stakken:

push (element) {
 this.items.push (element)
}

Testene bestås igen. Har du bemærket noget? Vi fortsætter med at kopiere og indsætte denne linje:

lad stak = ny stak (3)

Det er ret irriterende. Heldigvis for os har vi metoden BeforeEach, der giver os mulighed for at lave nogle opsætninger før hver testkørsel. Hvorfor ikke konstruere stakken i denne metode?

lad stak
beforeEach (() => {
 stak = ny stak (3)
})

Vigtigt: Stakken skal deklareres før hver. Faktisk, hvis du definerer den i metoden BeforeEach, vil stabelvariablen ikke defineres i alle testene, fordi den ikke er i det rigtige omfang.

Side note: Hvis vi ville have brugt førAllstead of beforeEach, ville vi også have en afterEach-metode. Faktisk vil stack-forekomsten blive delt på tværs af alle test. Det er et problem, da vi skubber på stakken, pop osv. Så vi bliver nødt til at nulstille stakken efter hver test:

afterEach (() => {
 stack.items = []
})

Men her, da vi opretter en ny stack-instans inden hver test, er denne afterEach-metode ikke nødvendig, det er dog godt at vide det.

Tilbage til test: Du kan fjerne initialiseringen af ​​stakken i alle testene nu. For dem, der føler sig lidt tabt, er her den fulde testfil op til dette punkt:

const Stack = kræver ('./ stack')
beskriv ('Stak', () => {
  lad stak
  beforeEach (() => {
    stak = ny stak (3)
  })
  it ('Bør konstruere stakken med en given kapacitet', () => {
    forventer (stack.items) .toEqual ([])
    forventer (stack.capacity) .toBe (3)
  })
  it ('Bør have en isEmpty-funktion, der returnerer sand, hvis stakken ellers er tom og falsk', () => {
    stack.items.push (2)
    forventer (stack.isEmpty ()). Tobe (falsk)
  })
  beskriv ('Stack.push', () => {
    it ('Bør tilføje et nyt element øverst på stakken', () => {
      stack.push (2)
      forvente (stack.items [stack.items.length - 1]). toBe (2)
    })
  })
})

Test af den returnerede værdi

Her er testen:

it ('Skal returnere det nye element, der er skubbet øverst i stakken', () => {
  lad elementPushed = stack.push (2)
  forventer (elementPushed) .toBe (2)
})

Når du kører testen, får du:

Forventet: 2
Modtaget: udefineret

Ja. Intet returneres indenfor push! Vi er nødt til at ordne det:

push (element) {
  this.items.push (element)
  returelement
}

Returnerer fuld, hvis stakken er fuld

I denne test skal vi først udfylde stakken, skubbe et element, kontrollere, at der ikke er skubbet noget på stakken, og at den returnerede værdi er Fuld.

det ('Skal vende tilbage fuldt, hvis man prøver at skubbe øverst i stakken, mens den er fuld', () => {
  stack.items = [1, 2, 3]
  lad element = stack.push (4)
  forvente (stack.items [stack.items.length - 1]). toBe (3)
  forventer (element) .toBe ( 'Fuld')
})

Du skal få denne fejl, når du kører testen:

Forventet: 3
Modtaget: 4

Så elementet blev skubbet. Det er ikke hvad vi ønsker. Vi skal først kontrollere, om stakken er fuld, før vi tilføjer noget:

push (element) {
  if (this.isFull ()) {
    returner 'Fuld'
  }
  
  this.items.push (element)
  returelement
}

Testene består nu. Vi er færdige med push!

Træning: pop og kig

Jeg tror, ​​du får, hvordan man gør testdrevet udvikling nu. Så som en øvelse, test og implementer pop og kig.

Nogle tip:

  • Pop ligner virkelig test-messig.
  • Peek ligner virkelig også poptest!
  • Indtil nu har vi ikke refactoreret koden, fordi der ikke var behov for at refactor den. I disse funktioner kan der være en måde at refaktorere din kode efter at have skrevet testene og få dem bestået. Ingen bekymringer, hvis du ændrer koden, testene er her for at fortælle dig, hvad der er galt.

Se ikke på løsningen herunder uden at prøve først. Den eneste måde at komme videre på er ved at prøve, eksperimentere og opbygge ting.

Løsning

Hvordan var øvelsen? Har du haft succes? Det håber jeg. Hvis ikke, skal du ikke bekymre dig, testning tager tid og kræfter, og test er svære at skrive til at begynde med.

Hvis du ser på filerne, kan du se, at jeg brugte en ternær tilstand i pop og kig. Det er noget, jeg refactored. Faktisk var den gamle implementering:

if (this.is Tom ()) {
  returner 'Tom'
}
returner dette.items.pop ()

Da TDD tillader os at refaktorere, efter at testene er skrevet, fandt jeg en kortere implementering uden at bekymre mig om opførslen af ​​mine tests.

Kør dine tests en sidste gang:

Ikke kun din stack testes, men du har også dokumenteret din kode virkelig godt. Bare ved at se på resultaterne af testene, kan vi straks se, hvordan din stabel opfører sig.

Skrivningstests forbedrer din kodes kvalitet dybt. Jeg håber, du nu forstår kraften ved testning og testdrevet udvikling.

Denne tutorial er en del af mit nye kursus: Build & Test a App from Scratch with Vue.js. Det lærer dig alt hvad du behøver at vide for at opbygge en fantastisk Vue-app: Vue-basics, API, Testing, Styling, Deploying og mere!

Hvis du kunne lide denne tutorial, så lad mig det vide ved at give mig nogle klapper. Det er altid dejligt at vide, at dit arbejde værdsættes.