Testing i Node.js

En testdrevet utviklingssyklus forenkler tankeprosessen med å skrive kode, gjør det enklere og raskere i det lange løp. Men bare å skrive tester er ikke nok av seg selv, å vite hva slags tester som skal skrives og hvordan man strukturerer kode for å passe til dette mønsteret, er hva det handler om. I denne artikkelen vil vi se på å bygge en liten app i Node.js etter et TDD-mønster.

Foruten enkle "enhetstester", som vi alle er kjent med; Vi kan også ha Node.js Async-kode som kjører, noe som legger til en ekstra dimensjon ved at vi ikke alltid vet hvilken rekkefølge funksjonene skal kjøre eller vi kan prøve å teste noe i tilbakeringing eller sjekke for å se hvordan en async-funksjon fungerer.

I denne artikkelen vil vi bygge en Node-app som kan søke etter filer som samsvarer med et gitt spørsmål. Jeg vet at det allerede er ting for dette (ack) men for å demonstrere TDD tror jeg det kan være et godt avrundet prosjekt.

Det første trinnet er åpenbart å skrive noen tester, men selv før må vi velge et testramme. Du kan bruke vanilje node, som det er en hevde bibliotek innebygd, men det er ikke mye i form av en testløper, og er ganske mye den eneste nødvendigheten.

Et annet alternativ og sannsynligvis min favoritt for allmenn bruk er Jasmine. Det er ganske selvstendig, du har ingen andre avhengigheter å legge til i skriptene dine, og syntaksen er veldig rent og lett å lese. Den eneste grunnen til at jeg ikke kommer til å bruke dette i dag, er fordi jeg tror Jack Franklin gjorde en utmerket jobb som dekker dette i sin siste Tuts + -serie her, og det er godt å vite alternativene dine slik at du kan velge det beste verktøyet for din situasjon.


Hva vi skal bygge

I denne artikkelen vil vi bruke den fleksible "Mocha" testløperen sammen med Chai påstandsbiblioteket.

I motsetning til jasmin som er mer som en hel testpakke i en pakke, tar Mocha bare seg av den overordnede strukturen, men har ingenting å gjøre med de faktiske påstandene. Dette gir deg mulighet til å holde konsekvent utseende når du kjører testene dine, men lar deg også kjøre hvilket påstandsbibliotek som passer best for din situasjon.

Så hvis du for eksempel skulle bruke vanilje 'assert' -biblioteket, kan du parre det med Mocha for å legge til noen struktur for tester.

Chai er et ganske populært alternativ, og handler også om alternativer og modularitet. Selv uten noen plugins, bruker du bare standard API, har du tre forskjellige syntakser du kan bruke avhengig av om du vil bruke en mer klassisk TDD-stil eller en mer oversatt BDD-syntaks.

Så nå som vi vet hva vi skal bruke, la oss komme inn i installasjonen.


Oppsettet

For å komme i gang, la oss installere Mocha globalt ved å kjøre:

npm installere -g mocha

Når det er ferdig, opprett en ny mappe for prosjektet vårt og kjør følgende inne i det:

npm installere chai

Dette vil installere en lokal kopi av Chai for vårt prosjekt. Deretter oppretter du en mappe som heter test inne i prosjektets katalog, da dette er standardstedet, vil Mocha se etter tester.

Det er ganske mye for oppsett, neste trinn er å snakke om hvordan du strukturerer appene dine når du følger en testdrevet utviklingsprosess.


Strukturere appen din

Det er viktig å vite at når du følger en TDD-tilnærming, må du ha tester og hva som ikke gjør det. En tommelfingerregel er å ikke skrive tester for andre folkeslag som allerede er testet kode. Hva jeg mener med dette er følgende: la oss si at koden din åpner en fil, du trenger ikke å teste personen fs funksjon, det er en del av languge og er tilsynelatende allerede godt testet. Det samme gjelder når du bruker tredjepartsbiblioteker, du bør ikke strukturere funksjoner som primært kaller disse typer funksjoner. Du skriver egentlig ikke tester for disse, og på grunn av dette har du hull i TDD-syklusen.

Nå, selvfølgelig, med hver programmeringsstil er det mange forskjellige meninger, og folk vil ha forskjellige syn på hvordan TDD. Men tilnærmingen jeg bruker, er at du lager individuelle komponenter som skal brukes i appen din, og hver av dem løser et unikt funksjonelt problem. Disse komponentene er bygget ved hjelp av TDD og sikrer at de fungerer som forventet, og du vil ikke bryte API-en. Deretter skriver du hovedskriptet ditt, som egentlig er all limkode, og trenger ikke å bli testet / kan ikke testes, i enkelte tilfeller.

Dette betyr også at de fleste av komponentene dine kan gjenbrukes i fremtiden, da de ikke har mye å gjøre direkte med hovedskriptet.

Etter det jeg nettopp sa, er det vanlig å lage en mappe som heter 'lib'hvor du legger alle de enkelte komponentene. Så til dette punktet bør du ha Mocha og Chai installert, og deretter en prosjektkatalog med to mapper: 'lib'og'test'.


Komme i gang med TDD

Bare hvis du er ny på TDD, trodde jeg det ville være en god ide å raskt dekke prosessen. Den grunnleggende regelen er at du ikke kan skrive noen kode med mindre testrunner forteller deg det.

I hovedsak skriver du hva koden din skal gjøre før du faktisk gjør det. Du har et veldig fokusert mål mens du kodes, og du kompromitterer aldri ideen din ved å bli sidesporet eller tenkt for langt framover. Dessuten, siden hele koden din vil ha en test tilknyttet den, kan du være sikker på at du aldri vil bryte appen din i fremtiden.

En test er i virkeligheten bare en erklæring om hva en funksjon forventes å gjøre når du kjører. Kjør du deretter testløperen din, som åpenbart vil mislykkes (siden du ikke har skrevet koden ennå) og så skriver du minimumsbeløpet av koden som trengs for å bestå den sviktende testen. Det er viktig å aldri hoppe over dette trinnet, fordi noen ganger vil en test passere selv før du legger til en kode, på grunn av annen kode du har i samme klasse eller funksjon. Når dette skjer, skrev du enten mer kode da du skulle til en annen test, eller dette er bare en dårlig test (vanligvis ikke spesifikk nok).

Igjen i henhold til vår regel over, hvis testen passerer med en gang, kan du ikke skrive noen kode fordi den ikke fortalte deg det. Ved kontinuerlig å skrive tester og deretter implementere funksjonene konstruerer du solide moduler som du kan stole på.

Når du er ferdig med å implementere og teste komponenten din, kan du da gå tilbake og reflektere koden for å optimalisere den og rydde den opp, men sørg for at refactoring ikke svikter noen av testene du har på plass, og enda viktigere, t legg til noen funksjoner som ikke er testet.

Hvert testbibliotek vil ha sin egen syntaks, men de følger vanligvis det samme mønsteret for å gjøre påstander og deretter sjekke om de passerer. Siden vi bruker Mokka og Chai, la oss ta en titt på begge deres syntakser som starter med Chai.


Mokka og Chai

Jeg vil bruke "Expect" BDD syntaks, fordi som jeg nevnte Chai kommer med noen få alternativer ut av boksen. Måten denne syntaksen fungerer på, begynner du ved å ringe forventningsfunksjonen, sende den objektet du vil ha påstand om, og så kjede du det med en bestemt test. Et eksempel på hva jeg mener kan være som følger:

forvente (4 + 5) .equal (9);

Det er den grunnleggende syntaksen, vi sier, forventer tillegg av 4 og 5 å like 9. Nå er dette ikke en god test fordi 4 og 5 vil bli lagt til av Node.js før funksjonen er til og med kalt, så vi tester egentlig mine matteferdigheter, men jeg håper du får den generelle ideen. Den andre tingen du bør merke, er denne syntaksen ikke veldig lesbar, når det gjelder flyt av en vanlig engelsk setning. Å vite dette, sa Chai følgende kjede getters som ikke gjør noe, men du kan legge dem til for å gjøre det mer oversatt og lesbart. Kjede getters er som følger:

  • til
  • være
  • vært
  • er
  • at
  • og
  • ha
  • med
  • av
  • samme
  • en
  • en

Ved hjelp av ovenstående kan vi omskrive vår tidligere test til noe slikt:

forvente (4 + 5) .to.equal (9);

Jeg liker virkelig følelsen av hele biblioteket, som du kan sjekke ut i deres API. Enkle ting som å ugjøre operasjonen er like enkelt som å skrive .ikke før testen:

forvente (4 + 5) .to.not.equal (10);

Så selv om du aldri har brukt biblioteket før, vil det ikke være vanskelig å finne ut hva en test prøver å gjøre.

Det siste jeg vil se på før vi går inn i vår første test, er hvordan vi strukturerer vår kode i Mokka

Mocha

Mokka er testløperen, så det bryr seg ikke veldig mye om de faktiske testene, hva det bryr seg om, er teststrukturen, fordi det er hvordan det vet hva som feiler og hvordan man lager opp resultatene. Måten du bygger opp, oppretter du flere beskrive blokkerer som skisserer de forskjellige komponentene i biblioteket ditt og deretter legger du til den blokkerer for å spesifisere en bestemt test.

For et raskt eksempel, la oss si at vi hadde en JSON klasse og at klassen hadde en funksjon å analysere JSON og vi ønsket å sørge for at parse-funksjonen kan oppdage en dårlig formatert JSON-streng. Vi kunne strukturere dette slik:

beskrive ("JSON", funksjon () beskrive ("parse ()", funksjon () det ("skal oppdage misdannede JSON strenger", funksjon () // Test går her););) ;

Det er ikke komplisert, og det er omtrent 80% personlig preferanse, men hvis du holder denne typen format, bør testresultatene komme ut i et veldig lesbart format.

Vi er nå klar til å skrive vårt første bibliotek, la oss begynne med en enkel synkronmodul for å bli bedre kjent med systemet. Vår app må kunne akseptere kommandolinjevalg for å angi ting som hvor mange nivåer av mapper vår app skal søke gjennom og selve spørringen.

For å ta vare på alt dette, vil vi opprette en modul som aksepterer kommandos streng og analyserer alle de inkluderte alternativene sammen med deres verdier.

Merkemodulen

Dette er et godt eksempel på en modul du kan bruke på nytt i alle kommandolinjeprogrammer, da dette problemet kommer opp mye. Dette vil bli en forenklet versjon av en faktisk pakke jeg har på npm kalt ClTags. Så å komme i gang, opprett en fil som heter tags.js inne i lib-mappen, og deretter en annen fil som heter tagsSpec.js innsiden av testmappen.

Vi må trekke inn Chai-forventningsfunksjonen, da det vil være påstandssyntaxen vi skal bruke, og vi må trekke inn selve koderfilen slik at vi kan teste den. Til sammen med noen innledende oppsett bør det se slik ut:

var forvent = kreve ("chai"). forvente; var tags = krever ("... /lib/tags.js"); Beskriv ("Tags", funksjon () );

Hvis du kjører "mocha" -kommandoen nå fra roten til prosjektet, bør alt passere som forventet. La oss nå tenke på hva vår modul vil gjøre; vi vil sende det kommandobaramargruppen som ble brukt til å kjøre appen, og da vil vi at det skal bygges et objekt med alle kodene, og det ville være fint om vi også kunne sende det som standard gjenstand for innstillinger, så hvis ingenting blir overstyrt, vi vil ha noen innstillinger som allerede er lagret.

Når du arbeider med koder, gir mange apper også snarveialternativer som bare er ett tegn, så la oss si at vi ønsket å sette dybden på søket vårt, slik at brukeren enten kunne spesifisere noe som --dybde = 2 eller noe som helst -d = 2 som burde ha samme effekt.

Så la oss bare begynne med de langformede kodene (for eksempel '--depth = 2'), til å begynne med, la oss skrive den første testen:

beskrive ("Tags", funksjon () describe ("# parse ()", funksjon () it ("burde tolke langformede koder", funksjon () var args = ["--depth = 4" --hello = world "]; var results = tags.parse (args); forvente (resultater) .to.have.a.property (" depth ", 4); forvente (resultater) .to.have.a.property ("Hei Verden"); ); ); );

Vi har lagt til en metode i vår testpakke som heter analysere og vi la til en test for langformede tagger. I denne testen opprettet jeg en eksempelkommando og la to påstander om de to egenskapene den skulle hente.

Kjører mokka nå, du bør få en feil, nemlig det tags har ikke en analysere funksjon. For å fikse denne feilen, la oss legge til en analysere Fungerer til taggermodulen. En ganske typisk måte å opprette en nodemodul på er slik:

eksport = module.exports = ; exports.parse = funksjon () 

Feilen sa at vi trengte en analysere metode slik at vi opprettet det, vi har ikke lagt til noen annen kode inne fordi den ikke fortalte oss det. Ved å holde fast i det minste, er du sikker på at du ikke vil skrive mer enn du skal og ende opp med uprøvd kode.

La oss nå kjøre Mokka igjen, denne gangen skulle vi få en feil som forteller oss at den ikke kan lese en eiendom som heter dybde fra en udefinert variabel. Det er fordi for tiden vår analysere funksjonen returnerer ikke noe, så la oss legge til noe kode slik at det vil returnere et objekt:

exports.parse = function () var options =  returalternativer; 

Vi beveger oss sakte langs, hvis du kjører Mokka igjen, bør de ikke være noen unntak som kastes, bare en ren feilmelding som sier at vår tomme gjenstand ikke har noen eiendom kalt dybde.


Nå kan vi komme inn i noen ekte kode. For vår funksjon å analysere taggen og legge den til i objektet vårt, må vi sykle gjennom argumentet og fjerne de doble bindestrekene ved starten av nøkkelen.

exports.parse = funksjon (args) var options =  for (var jeg i args) // Cycle through args var arg = args [i]; // Sjekk om Langformet tag hvis (arg.substr (0, 2) === "-") arg = arg.substr (2); // Sjekk for like signatur hvis (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nøkkel = arg.shift (); alternativer [key] = arg.join ("=");  returalternativer; 

Denne koden sykler gjennom listen over argumenter, sørger for at vi har å gjøre med en langformet tag, og splitt den deretter av den første like karakteren for å opprette nøkkel- og verdiparet for alternativobjektet.

Nå løser dette nesten vårt problem, men hvis vi kjører Mokka igjen, vil du se at vi nå har en nøkkel for dybde, men den er satt til en streng i stedet for et tall. Tall er litt enklere å jobbe med senere i vår app, så det neste koden vi må legge til er å konvertere verdier til tall når det er mulig. Dette kan oppnås med noen RegEx og parseInt fungere som følger:

 hvis (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nøkkel = arg.shift (); var verdi = arg.join ("="); hvis (/^[0-9]+$/.test(value)) value = parseInt (verdi, 10);  alternativer [nøkkel] = verdi; 

Kjører mokka nå, bør du få et pass med en test. Nummerkonvertering skal uten tvil være i sin egen test, eller i det minste nevnt i testdeklarasjonen, slik at du ved en feiltagelse ikke fjerner tallkonvertering påstanden; så bare add-on "add and convert numbers" til den erklæring for denne testen eller skille den inn i en ny den blokkere. Det avhenger egentlig om du vurderer denne "åpenbare standardadferd" eller en egen funksjon.


Nå som jeg har forsøkt å stresse gjennom hele denne artikkelen, når du ser en forbipasserende spesifikasjon, er det på tide å skrive flere tester. Den neste tingen jeg ønsket å legge til var standardoppsettet, så inne i tagsSpec filen la oss legge til følgende den blokkere rett etter den forrige:

 den ("burde tolke langformede koder og konvertere tall", funksjon () var args = ["--depth = 4", "--hello = world"]; var results = tags.parse (args); resultater) .to.have.a.property ("depth", 4); forvente (resultater) .to.have.a.property ("hei", "world");); den ("skal tilbakestilles til standard", funksjon () var args = ["--depth = 4", "--hello = world"]; var standard = dybde: 2, foo: "bar"; var results = tags.parse (args, default); var forventet = dybde: 4, foo: "bar", hei: "world"; expect (results) .to.deep.equal (expected););

Her bruker vi en ny test, den dype likningen som er bra for å matche to objekter for likeverdige verdier. Alternativt kan du bruke EQL test som er en snarvei, men jeg synes dette er tydeligere. Denne testen passerer to argumenter som kommandostrengen og sender to standard med en overlapping, slik at vi kan få en god spredning på testene.

Kjører Mokka nå, bør du få en slags diff, som inneholder forskjellene mellom hva som forventes og hva det egentlig har.


La oss nå fortsette tilbake til tags.js modul, og la oss legge til denne funksjonaliteten. Det er en ganske enkel løsning å legge til, vi trenger bare å godta den andre parameteren, og når den er satt til en objekt, kan vi erstatte standard tom objekt ved starten med dette objektet:

exports.parse = funksjon (args, standard) var options = ; hvis (standard type === "objekt" &&! (standard forekomst av Array)) options = standard

Dette vil bringe oss tilbake til en grønn tilstand. Det neste jeg vil legge til er evnen til bare å spesifisere en kode uten en verdi og la den virke som en boolsk. For eksempel, hvis vi bare har satt --searchContents eller noe sånt, vil det bare legge til det til vårt alternativvalg med en verdi på ekte.

Testen for dette ville se ut som følgende:

 det ("skal akseptere koder uten verdier som en bool", funksjon () var args = ["- searchIncentives"]; var results = tags.parse (args); expect (results) .to.have.a.property ("searchContents", true););

Kjører dette vil gi oss følgende feil akkurat som før:


Innsiden av til loop, da vi fikk en kamp for en lang formet tag, sjekket vi om det inneholdt et like tegn; Vi kan raskt skrive koden for denne testen ved å legge til en ellers klausul til det hvis setning og bare sette verdien til ekte:

 hvis (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nøkkel = arg.shift (); var verdi = arg.join ("="); hvis (/^[0-9]+$/.test(value)) value = parseInt (verdi, 10);  alternativer [nøkkel] = verdi;  else options [arg] = true; 

Den neste tingen jeg vil legge til er substitusjonene for kortmerkene. Dette blir den tredje parameteren til analysere funksjon og vil i utgangspunktet være et objekt med bokstaver og tilhørende utskiftninger. Her er spesifikasjonen for dette tillegget:

 den ("skal akseptere korte formaterte koder", funksjon () var args = ["-sd = 4", "-h"]; var erstatning = s: "searchContents", d: "depth" hei "; varresultater = tags.parse (args, , erstatninger); var forventet = searchContents: true, depth: 4, hei: true; expect (results) .to.deep.equal (expected); );

Problemet med shorthand-tagger er at de kan kombineres på rad. Hva jeg mener med dette, er ulikt de langformede kodene hvor hver enkelt er adskilt, med korte håndtak - siden de bare er bokstav lange - kan du ringe tre forskjellige ved å skrive -VGH. Dette gjør at parsing litt vanskeligere fordi vi fortsatt må tillate at like aktør for deg å legge til en verdi til den sistnevnte taggen, samtidig som du fortsatt må registrere de andre kodene. Men ikke bekymre deg, det er ingenting som ikke kan løses med nok popping og skifting.

Her er hele løsningen, fra begynnelsen av analysere funksjon:

exports.parse = funksjon (args, standard, erstatninger) var options = ; Hvis (type av standard === "objekt" &&! (standard forekomst av Array)) alternativer = standard hvis (type erstatter === "objekt" &&! (standard forekomst av Array)) for (var jeg i args)  var arg = args [i]; hvis (arg.charAt (0) === "-" && arg.charAt (1)! = "-") arg = arg.substr (1); hvis (arg.indexOf ("=")! == -1) arg = arg.split ("="); var keys = arg.shift (); var verdi = arg.join ("="); arg = keys.split (""); var nøkkel = arg.pop (); hvis (erstatter.hasOwnProperty (nøkkel)) nøkkel = erstatning [nøkkel];  args.push ("-" + key + "=" + verdi);  ellers arg = arg.split ("");  arg.forEach (funksjon (nøkkel) hvis (erstatter.hasOwnProperty (nøkkel)) nøkkel = erstatninger [tast]; args.push ("-" + nøkkel);); 

Det er mye kode (i sammenligning), men alt vi egentlig gjør, er å dele argumentet med et like tegn, og deretter dele nøkkelen i de enkelte bokstaver. Så for eksempel hvis vi passerte -gj = asd vi ville dele asd inn i en variabel som heter verdi, og så splittet vi gj delen i individuelle tegn. Den siste karakteren (j i vårt eksempel) blir nøkkelen for verdien (asd) mens andre bokstaver før den, vil bare bli lagt til som vanlige boolske koder. Jeg ville ikke bare behandle disse kodene nå, bare hvis vi endret implementeringen senere. Så det vi gjør, er bare å konvertere disse korte håndtakene til den langformede versjonen, og deretter la skriptet håndtere det senere.

Kjører Mokka igjen vil ta oss tilbake til våre strålende grønne resultater av fire tester som passerer for denne modulen.

Nå er det noen flere ting vi kan legge til i denne taggenmodulen for å gjøre det nærmere npm-pakken, for eksempel muligheten til å lagre vanlige tekstargumenter for ting som kommandoer eller evnen til å samle all tekst på slutten, for en spørringseiendom. Men denne artikkelen er allerede lang, og jeg vil gjerne fortsette å implementere søkefunksjonen.


Søkemodulen

Vi har nettopp gått gjennom å skape en modul trinnvis etter en TDD-tilnærming, og jeg håper du har ideen og følelsen av hvordan du skriver slik. Men for å holde denne artikkelen beveger seg, for resten av artikkelen, vil jeg fremskynde testprosessen ved å gruppere ting sammen og bare vise deg de endelige versjonene av testene. Det er mer en veiledning til forskjellige situasjoner som kan komme opp og hvordan man skriver tester for dem.

Så bare opprett en fil som heter search.js inne i lib-mappen og a searchSpec.js filen inne i testmappen.

Deretter åpner du spesifikasjonsfilen og lar oss sette opp vår første test som kan fungere for å få en liste over filer basert på a dybde parameter, dette er også et godt eksempel på tester som krever litt eksternt oppsett for at de skal jobbe. Når du arbeider med eksterne objekt-lignende data eller i våre saksfiler, vil du ha et forhåndsdefinert oppsett som du vet vil fungere med tester, men du vil heller ikke legge til falsk info på systemet ditt.

Det er i utgangspunktet to alternativer for å løse dette problemet, du kan enten forsøke dataene, som jeg nevnte ovenfor hvis du har å gjøre med språkens egne kommandoer for lasting av data, trenger du ikke nødvendigvis å teste dem. I slike tilfeller kan du bare gi de "hentede" dataene og fortsette med testingen din, som om hva vi gjorde med kommandostrengen i tags-biblioteket. Men i dette tilfellet tester vi rekursiv funksjonalitet som vi legger til i språkfilerens lesefunksjoner, avhengig av spesifisert dybde. I slike tilfeller må du skrive en test, og vi må derfor opprette noen demofiler for å teste filavlesningen. Alternativet er å kanskje stubbe fs Fungerer for å bare løpe, men ikke gjøre noe, og da kan vi telle hvor mange ganger vår falske funksjon kjørte eller noe slikt (sjekk ut spioner), men for vårt eksempel skal jeg bare lage noen filer.

Mokka gir funksjoner som kan kjøre både før og etter testene dine, slik at du kan utføre slike ekstern oppsett og opprydding rundt testene dine.

For eksempel vil vi lage et par testfiler og mapper på to forskjellige dybder, slik at vi kan teste ut den funksjonaliteten:

var forvent = kreve ("chai"). forvente; var søk = krever ("... /lib/search.js"); var fs = krever ("fs"); beskrive ("Søk", funksjon () beskrive ("# scan ()", funksjon () før (funksjon () hvis ! fs.existsSync (". test_files")) fs.mkdirSync (". test_files "); fs.writeFileSync (". test_files / a "," "); fs.writeFileSync (". test_files / b "," "); fs.mkdirSync (" .test_files / dir "); fs.writeFileSync .test_files / dir / c "," "); fs.mkdirSync (". test_files / dir2 "); fs.writeFileSync (". test_files / dir2 / d ";" ");); etter fs.unlinkSync (". test_files / dir / c"); fs.rmdirSync (". test_files / dir"); fs.unlinkSync (". test_files / dir2 / d"); fs.rmdirSync (".test_files / dir2 "); fs.unlinkSync (". test_files / a "); fs.unlinkSync (". test_files / b "); fs.rmdirSync (". test_files "););;;);

Disse vil bli kalt basert på beskrive blokkere de er i, og du kan til og med kjøre kode før og etter hver den blokkere ved hjelp av beforeEach eller afterEach i stedet. Funksjonene selv bruker bare standardknutekommandoer for å opprette og fjerne filene henholdsvis. Deretter må vi skrive den faktiske testen. Dette bør gå rett ved siden av etter funksjon, fortsatt inne i beskrive blokkere:

 det ("skal hente filene fra en katalog", funksjon (ferdig) search.scan (". test_files", 0, funksjon (feil, flist) forventer (flist) .to.deep.equal ([".test_files / a "," .test_files / b "," teste_files / dir / c "," .test_files / dir2 / d "]); ferdig ();;;);

Dette er vårt første eksempel på å teste en async-funksjon, men som du kan se er det like enkelt som før; alt vi trenger å gjøre er å bruke ferdig funksjon Mocha gir i den erklæringer for å fortelle det når vi er ferdige med denne testen.

Mokka vil automatisk oppdage hvis du angav ferdig variabel i tilbakeringingen, og det vil vente på at det blir kalt, slik at du kan teste asynkron kode veldig enkelt. Det er også verdt å nevne at dette mønsteret er tilgjengelig gjennom Mocha, du kan for eksempel bruke dette i før eller etter Fungerer hvis du trenger å sette opp noe asynkront.

Neste vil jeg skrive en test som gjør at dybdeparameteren fungerer hvis den er satt:

 den ("skal stoppe ved en bestemt dybde", funksjon (ferdig) search.scan (". test_files", 1, funksjon (feil, flist) forventer (flist) .to.deep.equal ([".test_files / en "," .test_files / b ",]); ferdig ();););

Ingenting annet her, bare en annen vanlig test. Kjører dette i Mokka vil du få en feil at søket ikke har noen metoder, i utgangspunktet fordi vi ikke har skrevet noe i det. Så la oss gå legge til en oversikt med funksjonen:

var fs = krever ("fs"); eksport = module.exports = ; exports.scan = funksjon (dir, dybde, ferdig) 

Hvis du kjører Mokka igjen, vil den stoppe venter på at denne async-funksjonen kommer tilbake, men siden vi ikke har ringt tilbakekallingen i det hele tatt, vil testen bare timeout. Som standard bør det gå ut etter omtrent to sekunder, men du kan justere dette ved hjelp av this.timeout (millisekunder) innsiden av en beskrivelse eller blokkering, for å justere tidsavbruddene henholdsvis.

Denne skannefunksjonen skal ta en sti og dybde, og returnere en liste over alle filene den finner. Dette er faktisk litt vanskelig når du begynner å tenke på hvordan vi i hovedsak rekurserer to forskjellige funksjoner sammen i en enkelt funksjon. Vi må rekruttere gjennom de forskjellige mappene, og da må mappene skanne seg selv og bestemme seg for å gå videre.

Å gjøre dette synkront er greit fordi du kan snakke om det en for en, sakte å fullføre ett nivå eller en gang om gangen. Når det gjelder en async-versjon, blir det litt mer komplisert fordi du ikke bare kan gjøre en for hver sløyfe eller noe, fordi det ikke vil pause i mellom mapper, vil de alle i det vesentlige kjøre på samme tid hver returnere forskjellige verdier, og de ville sortere overskrive hverandre.

For å få det til å fungere, må du lage en slags stabel der du kan asynkront behandle en om gangen (eller alt på en gang hvis du bruker en kø istedenfor) og deretter holde orden på den måten. Det er en veldig spesifikk algoritme, så jeg bare beholder en utdrag av Christopher Jeffrey som du finner på Stack Overflow. Det gjelder ikke bare for å laste filer, men jeg har brukt dette i en rekke applikasjoner, i utgangspunktet alt der du trenger å behandle en rekke objekter en om gangen ved å bruke async-funksjoner.

Vi må endre det litt, fordi vi ønsker å ha en dybdealternativ, hvordan dybdealternativet fungerer, er du angitt hvor mange nivåer mapper du vil sjekke, eller null for å gjenopprette på ubestemt tid.

Her er den fullførte funksjonen ved hjelp av koden:

exports.scan = funksjon (dir, dybde, ferdig) deep--; varresultater = []; fs.readdir (dir, funksjon (feil, liste) hvis (err) returneres ferdig (err); var i = 0; (funksjon neste () var fil = liste [i ++] null, resultater); fil = dir + '/' + fil; fs.stat (fil, funksjon (feil, stat) if (stat && stat.isDirectory ()) hvis (dybde! == 0) var ndepth = (dybde> 1)? dybde-1: 1; exports.scan (fil, ndepth, funksjon (feil, res) results = results.concat (res); next ();); else next ; ellers results.push (fil); neste (););) ();); ;

Mokka bør nå bestå begge testene. Den siste funksjonen vi trenger å implementere, er den som vil akseptere en rekke stier og et søkeord og returnere alle kamper. Her er testen for det:

 Beskriv ("# match ()", funksjon () det ("skal finne og returnere kamper basert på en spørring", funksjon () var files = ["hello.txt", "world.js", "another. js "]; var results = search.match (". js ", filer); forvente (resultater) .to.deep.equal ([" world.js "," another.js "]); results = search.match ("hei", filer); forvent (resultater) .to.deep.equal (["hello.txt"]);););

Og sist men ikke minst, la oss legge til funksjonen til search.js:

export.match = funksjon (spørring, filer) var matches = []; files.forEach (funksjon (navn) if (name.indexOf (spørring)! == -1) matches.push (navn);); retur kamper; 

Bare for å forsikre deg om at du kjører Mokka igjen, bør du ha totalt syv tester som passerer.



Sette alt sammen

Det siste trinnet er å virkelig skrive limkoden som trekker alle modulene sammen; så i roten av prosjektet legger du til en fil som heter app.js eller noe sånt, og legg til følgende inne:

#! / usr / bin / env node var tags = krever ("./ lib / tags.js"); var search = krever ("./ lib / search.js"); var standard = sti: ".", spørring: "", dybde: 2 var erstatning = p: "sti", q: "spørring", d: "dybde", h: "hjelp" .parse (prosess.argv, standardinnstillinger, erstatninger); hvis (tags.help) console.log ("Bruk: ./app.js -q = query [-d = depth] [-p = path]");  else search.scan (tags.path, tags.depth, funksjon (feil, filer) search.match (tags.query, filer) .forEach (funksjon (fil) console.log (file);); ); 

Ingen faktisk logikk skjer her, egentlig, vi knytter bare utgangspunktet de forskjellige modulene sammen for å få de ønskede resultatene. Jeg pleier ikke å teste denne koden, da det bare er limkode som allerede er testet.

Du kan nå gjøre skriptet kjørbart (chmod + x app.js på et Unix-system) og kjør det slik:

./app.js -q = ". js"

Eventuelt tilpasse noen av de andre plassholderne vi installerer.



Konklusjon

I denne artikkelen har vi bygget en hel filsøk app, om enn en enkel en, men jeg tror det demonstrerer prosessen som helhet ganske bra.

Noen personlige råd fremover; hvis du skal gjøre mye TDD, sett opp miljøet ditt. Mye av overhead tiden folk forbinder med TDD skyldes at de måtte fortsette å bytte vinduer rundt, åpne og lukke forskjellige filer, deretter løpe tester og gjenta dette 80 dusin ganger om dagen. I så fall forstyrrer arbeidsflyten din produktiviteten. Men hvis du har redigeringsoppsettet ditt, som om du enten har tester og kode side om side, eller din IDE støtter å hoppe frem og tilbake, sparer det massevis av tid. Du kan også få testene dine til å kjøre automatisk ved å ringe det med