Bedre CoffeeScript Testing med mokka

Nylig har jeg gjort en betydelig mengde CoffeeScript-arbeid. Et problem jeg kjørte tidlig på, var testing: Jeg ville ikke manuelt konvertere CoffeeScript til JavaScript før jeg kunne teste den. I stedet ønsket jeg å teste fra CoffeeScript direkte. Hvordan slutte jeg med å gjøre det? Les videre for å finne ut!

Publisert opplæring

Noen få uker besøker vi noen av leserens favorittinnlegg fra hele historien til nettstedet. Denne opplæringen ble først publisert i november 2012.


Du må ha Node.js og Node Package Manager installert.

Før vi fortsetter, vil jeg påpeke at du må ha en anstendig kjennskap til CoffeeScript for denne opplæringen; Jeg vil ikke forklare biter og stykker her. Hvis du er interessert i CoffeeScript, bør du sjekke ut CoffeeScript-knappene som er tilgjengelige her på Nettuts +, eller CoffeeScript-dokumentasjonen.

I tillegg må du ha Node.js og Node Package Manager (npm) installert for denne opplæringen. Hvis du ikke har dem installert, ingen bekymringer: gå over til nodejs.org og last ned installasjonsprogrammet for plattformen din; da, vel, installer det!


Møte Mokka og Chai

Vi skal bygge begynnelsen av en todo-liste søknad (cliché, jeg vet). Disse blir CoffeeScript-klasser. Deretter skal vi skrive noen tester med Mocha og Chai for å teste den funksjonaliteten.

Hvorfor både Mocha og Chai? Vel, Mocha er en testing rammeverk, men det inkluderer ikke selve påstanden komponent. Det høres kanskje rart ut: tross alt er det ikke mye mer til et testbibliotek, er det? Vel, det er, i Mokka saken. Funksjonene som brakte meg til biblioteket er to ganger: muligheten til å kjøre tester fra kommandolinjen (i stedet for å ha en HTML-side for å kjøre dem i nettleseren), og muligheten til å kjøre test i CoffeeScripts, uten å måtte konvertere som kodes til JavaScript (som minst manuelt: Mokka gjør det bak kulissene). Det er også andre funksjoner som jeg ikke snakker om her, inkludert:

  • Du kan enkelt teste asynkron kode.
  • Du kan se etter spesielt sakte tester.
  • Du kan sende resultatene i en rekke forskjellige formater.

Og videre og videre. Se mer på Mokka-hjemmesiden. For å installere Mocha bare kjøre npm installere -g mocha, og du er satt.

Som for Chai: det er et flott påstandsbibliotek som tilbyr grensesnitt for å gjøre både BDD og TDD; Du kan bruke det både i nettleseren eller på kommandolinjen via node, slik bruker vi det i dag. Installer det for Node, via npm installere -g chai.

Nå som vi har installert bibliotekene våre, la oss begynne å skrive noen kode.


Sette opp vårt prosjekt

La oss begynne med å sette opp et mini-prosjekt. Lag en prosjektmappe. Deretter oppretter du to mapper i den ene: src, og test. Vår CoffeeScript kode vil gå inn i src mappe, og våre tester vil gå inn, du gjettet det, tester mappe. Mokka ser etter en test mappe som standard, så ved å gjøre dette, lagrer vi oss selv litt senere.

Mokka ser etter en test mappen som standard.

Vi skal lage to CoffeeScript-klasser: Oppgave, som vil være en todo gjenstand, og Oppgaveliste, som vil være en liste over todo elementer (ja, det er mer enn en matrise). Vi legger dem begge i src / task.coffee fil. Deretter vil testene for dette være i test / taskTest.coffee. Selvfølgelig kan vi dele dem inn i egne filer, men vi skal bare ikke gjøre det i dag.

Vi må begynne med å importere Chai-biblioteket og aktivere BDD-syntaksen. Dette er hvordan:

chai = krever 'chai' chai.should ()

Ved å ringe chai.should metode, vi legger faktisk til en bør eiendom til Object.prototype. Dette tillater oss å skrive tester som leser slik:

task.name.should.equal "litt streng"

Hvis du foretrekker TDD-syntaksen, kan du gjøre dette:

Forvent = Chai.expect

... som lar deg skrive tester som dette:

forventer (oppgave.navn) .til ekvivalent "litt streng"

Vi må faktisk bruke begge disse, som du vil se; Vi bruker imidlertid BDD-syntaksen så mye som mulig.

Nå må vi importere vår Oppgave og Oppgaveliste klasser:

TaskList, List = krever '... / src / task'

Hvis du ikke er kjent med denne syntaksen, er det CoffeeScripts ødelagte oppgave på jobben, så vel som noen av dens objekt, bokstavelig sukker. I utgangspunktet vår krever anrop returnerer et objekt med to egenskaper, som er våre klasser. Denne linjen trekker dem ut av objektet og gir oss to variabler som heter Oppgave og Oppgaveliste, som hver peker til den respektive klassen.


Skrive våre første tester

Flott! Nå, hva med en test? Skjønnheten i Mocha-syntaksen er at dens blokker (beskrive og den) er identiske med jasmines (begge er svært lik RSpec). Her er vår første test:

Beskriv 'Oppgaveeksempel', -> oppgave1 = oppgave2 = null det 'skal ha et navn', -> oppgave1 = ny oppgave 'feed katten' oppgave1.navn.should.equal 'feed katten'

Vi starter med en beskrive ring: alle disse testene er for en test-forekomst. Ved innstilling test1 = test2 = null utenfor våre individuelle tester, kan vi bruke disse verdiene for flere tester.

Så, i vår første test, oppretter vi bare en oppgave og sjekker for å se at navnet på eiendommen har den riktige verdien. Før du skriver koden for dette, la oss legge til to flere tester:

det skal være i utgangspunktet ufullstendig », -> oppgave1.status.should.equal 'ufullstendig' det 'skal kunne fullføres', -> task1.complete (). should.be.true task1.status.should.equal 'fullstendig'

Ok, la oss kjøre disse testene for å sikre at de svikter. For å gjøre dette, la oss åpne en ledetekst og cd til prosjektmappen din. Kjør deretter denne kommandoen:

mocha - compilers kaffe: kaffe-script

Mokka sjekker ikke for CoffeeScript som standard, så vi må bruke --kompilatorer flagg for å fortelle Mocha hvilken kompilator å bruke hvis den finner en fil med kaffe filutvidelse. Du bør få feil som ser slik ut:

Hvis, i stedet for å se det, får du feilen Kan ikke finne modulen '... / src / task', det er fordi din src / task.coffee filen eksisterer ikke ennå. Lag nevnte fil, og du burde få den nevnte feilen.


Koding av våre første funksjoner

Vel, nå at vi har sviktende tester, er det på tide å skrive koden, riktig? Åpne det src / task.coffee fil og la oss få sprekker.

klasse Oppgavekonstruktør: (@name) ->

Bare dette er nok til å få vår første testkjøring. Hvis du ikke er kjent med parameterparametrene, setter det bare hva verdien er sendt til ny oppgave til @Navn (eller this.name) eiendom. La oss imidlertid legge til en annen linje til den konstruktøren:

@status = 'ufullstendig'

Det er bra. Nå, gå tilbake til terminalen og kjøre testene våre igjen. Du finner det - vent et sekund, ingenting er forandret! Hvorfor går ikke våre første to tester forbi?

Et enkelt problem, faktisk. Fordi CoffeeScript-kompilatoren bryter inn koden i hver fil i en IIFE (eller en egenaktiverende anonym funksjon), må vi "eksportere" alt vi vil være tilgjengelig fra andre filer. I nettleseren vil du gjøre noe sånt window.Whatever = hva som helst. For Node, kan du enten bruke global eller eksport. Vi skal bruke eksport, siden 1) det anses best praksis, og 2) det er det vi forberedte på når vi satte opp våre tester (husk vår krever anrop?). Derfor, på slutten av vår task.coffee fil, legg til dette:

root = eksport? vindu root.Task = Oppgave

Med det på plass, bør du oppdage at to av våre tre tester nå går forbi:

For å få den siste testen til å passere, må vi legge til en fullstendig metode. Prøv dette:

fullfør: -> @status = 'fullfør' true

Nå passerer alle tester:

Nå er det en god tid å nevne at Mokka har en rekke forskjellige rapporter: Dette er bare forskjellige måter å levere testresultatene på. Du kan kjøre mocha - reporters å se alternativene dine:

Som standard bruker Mocha prikk reporteren. Men jeg foretrekker spec reporteren, så jeg takker -R spec på slutten av kommandoen (-R er reporter-innstillingen flagg).


Legge til en funksjon

La oss legge til en funksjon i vår Oppgave Klasse: Vi lar oppgaver være avhengige av andre oppgaver. Hvis oppgaven "foreldre" ikke er fullført, kan ikke oppgaven "barn" gjøres. Vi beholder denne funksjonen enkel og tillater at oppgaver kun har én underoppgave. Vi vil heller ikke sjekke om rekursivitet, så mens det blir mulig å sette to oppgaver for å være foreldre og barn til hverandre, vil det gjøre begge oppgavene ufullstendige.

Prøver først!

det skal kunne være avhengig av en annen oppgave, -> task1 = new Task 'wash dishes' task2 = ny oppgave 'tørre retter' task2.dependsOn task1 task2.status.should.equal 'dependent' task2.parent.should .equal task1 task1.child.should.equal task2 den 'bør nekte fullføring det er avhengig av en ufullstendig oppgave', -> (-> task2.complete ()). should.throw "Avhengige oppgave" vaskefat "er ikke fullført ."

Oppgave forekomster kommer til å ha a kommer an på metode, hvilke oppgaver oppgaven som blir deres overordnet. Oppgaver som har en overordnet oppgave, skal ha status som "avhengig". Også begge oppgaver får enten a forelder eller barn eiendom som peker på riktig oppgaveeksempel.

I den andre testen der, sier vi at en oppgave med en ufullstendig foreldreoppgave burde kaste en feil når det er fullstendig Metoden kalles. Legg merke til hvordan testsyntaxen fungerer: vi må ringe bør av en funksjon, og ikke resultatet av funksjonen: derfor vikler vi funksjonen i parentes. På denne måten kan testbiblioteket ringe selve funksjonen og se etter feilen.

Kjør disse testene, og du vil se at begge mislykkes. Kodingstid!

dependsOn: (@parent) -> @ parent.child = @ @status = 'avhengig'

Igjen, veldig enkelt: Vi har bare satt opp oppgaveparameteren til foreldreoppgaven, og gi den en barnegenskap som peker på dette oppgavetilfelle. Deretter setter vi statusen til dette Oppgave å være "avhengig".

Hvis du kjører dette nå, vil du se at en av våre tester går forbi, men den andre er ikke: det er fordi vår fullstendig Metoden sjekker ikke for en ufullført foreldreoppgave. La oss endre det.

fullfør: -> hvis @parent? og @ parent.status er ikke "fullført" kaste "Avhengig oppgave" #@parent.name 'er ikke fullført. " @status = 'fullfør' true

Her er det ferdige fullstendig Metode: Hvis det er en overordnet oppgave, og det er ikke fullført, kaster vi en feil. Ellers fullfører vi oppgaven. Nå skal alle tester passere.


Bygg oppgavelinjen

Deretter skal vi bygge Oppgaveliste klasse. Igjen begynner vi med en test:

beskriv 'TaskList', -> taskList = null det 'skal starte uten oppgaver', -> taskList = new TaskList taskList.tasks.length.should.equal 0 taskList.length.should.equal 0

Dette er gamle hue til deg nå: vi lager en Oppgaveliste objekt og sjekke dens oppgaver og lengde egenskaper for å sikre at de begge er null. Som du kanskje gjetter, oppgaver er en matrise som holder oppgavene, mens lengde er bare en praktisk egenskap som vi vil oppdatere når du legger til eller fjerner oppgaver; det sparer oss bare fra å måtte skrive list.tasks.length.

For å få denne testen til å passere, gjør vi denne konstruktøren:

klasse TaskList constructor: () -> @tasks = [] @length = 0

God start, og det får testen vår.

Vi vil være i stand til å legge til oppgaver i en oppgaveliste, ikke sant? Vi har en Legg til metode som kan ta enten a Oppgave eksempel eller en streng som den vil konvertere til a Oppgave forekomst.

Våre tester:

det skal akseptere nye oppgaver som oppgaver, -> oppgave = ny oppgave 'kjøp melk' taskList.add oppgave taskList.tasks [0] .name.should.equal 'buy milk' taskList.length.should.equal 1 det ' bør godta nye oppgaver som streng ', -> taskList.add' ta ut søppel 'taskList.tasks [1] .name.should.equal' ta ut søppel 'taskList.length.should.equal 2

Først legger vi til en faktisk Oppgave objekt, og kontroller taskList.tasks array for å bekrefte at den er lagt til. Deretter legger vi til en streng, og sørger for at a Oppgave objekt med riktig navn ble lagt til i oppgaver array. I begge tilfeller kontrollerer vi lengden på oppgaveliste også, for å sørge for at det blir eiendommen oppdatert.

Og funksjonen:

legg til: (oppgave) -> hvis type av oppgave er 'string' @ tasks.push ny Oppgaveoppgave ellers @ tasks.push oppgave @length = @ tasks.length

Ganske selvforklarende, tror jeg. Og nå passerer våre tester:

Selvfølgelig vil vi kanskje fjerne oppgaver fra listen vår, til høyre?

det 'burde fjerne oppgaver', -> i = taskList.length - 1 taskList.remove taskList.tasks [jeg] forventer (taskList.tasks [i]). to.not.be.ok

Først kaller vi fjerne metode (ennå å bli skrevet, selvfølgelig), bestått den siste oppgaven for tiden i listen. Jo, vi kunne bare skrive inn indeksen 1, men jeg har gjort det på denne måten fordi det gjør denne testen fleksibel: Hvis vi endret våre tidligere tester eller lagt til flere tester over dette, må det kanskje endres. Selvfølgelig må vi fjerne den siste fordi ellers vil oppgaven etter det ta sin plass og det vil være noe i den indeksen når vi forventer at det ikke skal være noe.

Og snakker om å forvente, merk at vi bruker forvente funksjon og syntaks her i stedet for vår vanlige bør. Dette er fordi taskList.tasks [i] vil være udefinert, som ikke arver fra Object.prototype, og derfor kan vi ikke bruke bør.

Å, ja, vi trenger fortsatt å skrive det fjerne funksjon:

fjern: (oppgave) -> i = @ tasks.indexOf oppgave @tasks = @tasks [0 ... i] .concat @tasks [i + 1 ...] hvis jeg> -1 @length = @ tasks.length

Noen fancy array footwork kombinert med CoffeeScript's rekkevidde og array splicing shorthand lukker denne avtalen for oss. Vi splitter ganske enkelt av alle elementene før den som skal fjernes og alle elementene etter det; vi concat de to arrays sammen. Selvfølgelig oppdaterer vi @lengde tilsvarende. Kan du si "bestått test"?

La oss gjøre en ting til. Vi ønsker å skrive ut vår (relativt) flotte liste over gjeldende oppgaver. Dette vil bli vår mest komplekse (eller i det minste, vår lengste) testen ennå:

 det skal skrive ut listen ', -> taskList = new TaskList task0 = new Task' buy milk 'task1 = ny Oppgave' gå til butikk 'task2 = ny Oppgave' en annen oppgave 'task3 = ny Oppgave' underoppgave 'oppgave4 = ny oppgave 'sub-oppgave' oppgaveliste.add oppgave0 oppgaveliste.add oppgave1 oppgaveliste.add oppgave2 oppgaveliste.add oppgave3 oppgaveliste.add oppgave4 oppgave0differentiell oppgave1 oppgave4oppgaveoppgaveoppgave3oppgave3oppgaveoppgaveoppgave2oppgave1.complete () ønsketOutput = " "" Oppgaver - kjøp melk (avhenger av "gå til butikk") - gå til butikk (fullført) - en annen oppgave - underoppgave (avhenger av en annen oppgave) - sub-oppgave (avhenger av 'underoppgave ') "" "taskList.print (). should.equal ønsketOutput

Hva foregår her? Først oppretter vi en ny Oppgaveliste objekt slik at vi starter fra bunnen av. Deretter oppretter vi fem oppgaver og legger til dem oppgaveliste. Deretter satte vi opp noen avhengigheter. Til slutt fullfører vi en av våre oppgaver.

Vi bruker CoffeeScript's heredok-syntaks for å lage en multi-line streng. Som du ser, holder vi det ganske enkelt. Hvis en oppgave har en overordnet oppgave, blir det nevnt i parentes etter oppgavenavnet. Hvis en oppgave er fullført, setter vi det også.

Klar til å skrive funksjonen?

print: -> str = "Oppgaver \ n \ n" for oppgave i @tasks str + = "- # task.name" str + = "(avhenger av '# task.parent.name')" hvis task.parent? str + = '(fullført)' hvis oppgavestatus er 'fullstendig' str + = "\ n" str

Det er faktisk ganske greit: vi ser bare over @tasks array og legg dem til en streng. Hvis de har en forelder, legger vi til det, og hvis de er komplette, legger vi til det også. Legg merke til at vi bruker modifikasjonsskjemaet til hvis uttalelse, for å stramme opp koden vår. Deretter returnerer vi strengen.

Nå skal alle våre tester passere:


Wrapping Up

Prøv å legge til noen få funksjoner for å få tak i det hele.

Det er omfanget av vårt lille prosjekt i dag. Du kan laste ned koden fra toppen av denne siden; Faktisk, hvorfor prøver du ikke å legge til noen få funksjoner for å få tak i det hele? Her er noen ideer:

  • Forhindre Oppgave forekomster fra å være avhengige av hverandre (rekursive avhengigheter).
  • Lage Tasklist :: legge Metoden kaster en feil hvis den mottar noe annet enn en streng eller en Oppgave gjenstand.

I disse dager finner jeg CoffeeScript mer og mer attraktivt, men den største ulempen er at det må kompileres til JavaScript før det er nyttig. Jeg er takknemlig for noe som negerer noe av den arbeidsflytbryteren, og Mokka gjør det definitivt. Selvfølgelig er det ikke perfekt (siden det samler seg til JS før du kjører koden, stemmer ikke linjene i feilene overens med dine CoffeeScript-linjenumre), men det er et skritt i riktig retning for meg!

Hva med deg? Hvis du bruker CoffeeScript, hvordan har du gjort testing? Gi meg beskjed i kommentarene.