Testning i Node.js

En testdriven utvecklingscykel förenklar tankeprocessen för att skriva kod, gör det enklare och snabbare på lång sikt. Men bara skriva tester är inte tillräckligt för sig själv, att veta vilka typer av tester som ska skrivas och hur man strukturerar kod för att överensstämma med detta mönster är vad det handlar om. I den här artikeln kommer vi att titta på att bygga en liten app i Node.js efter ett TDD-mönster.

Förutom enkla "enhetstest", som vi alla är bekanta med Vi kan också ha Node.js Async-kod som körs, vilket lägger till en extra dimensionera genom att vi inte alltid vet vilken ordning funktionerna kommer att springa i eller vi kan försöka testa något i en återuppringning eller checka för att se hur en asyncfunktion fungerar.

I den här artikeln kommer vi att bygga en nod-app som kan söka efter filer som matchar en given fråga. Jag vet att det redan finns saker för detta (ack) men för att visa TDD tycker jag att det kan vara ett väl avrundat projekt.

Det första steget är självklart att skriva några tester, men även innan det måste vi välja ett testramverk. Du kan använda vaniljnod, eftersom det finns en hävda bibliotek inbyggt, men det är inte mycket i form av en testlöpare, och är ganska mycket det nödvändiga.

Ett annat alternativ och förmodligen min favorit för allmän användning är Jasmine. Det är ganska självständigt, du har inga andra beroenden att lägga till i dina skript och syntaxen är mycket ren och lätt att läsa. Den enda anledningen till att jag inte kommer att använda det här idag är att jag tror att Jack Franklin gjorde ett utmärkt jobb som täcker detta i hans senaste Tuts + -serier här och det är bra att känna till dina alternativ så att du kan välja det bästa verktyget för din situation.


Vad vi ska bygga

I den här artikeln kommer vi att använda den flexibla "Mocha" testlöparen tillsammans med Chai assertionsbiblioteket.

Till skillnad från jasmin som är mer som en hel testpaket i ett paket, tar Mocha bara hand om den övergripande strukturen men har ingenting att göra med de faktiska påståenden. Detta gör det möjligt för dig att hålla en konsekvent utseende när du kör dina test, men låter dig också springa vilket påståendebibliotek som bäst passar din situation.

Så till exempel, om du skulle använda vaniljens "assert" -bibliotek, kan du para det med mocka för att lägga till en viss struktur för dina test.

Chai är ett ganska populärt alternativ, och handlar också om alternativ och modularitet. Även utan några plugins, använder du bara standard API, du har tre olika syntaxer du kan använda beroende på om du vill använda en mer klassisk TDD-stil eller en mer verbod BDD-syntax.

Så nu när vi vet vad vi ska använda, låt oss komma in i installationen.


Upplägget

För att komma igång, låt oss installera Mocha globalt genom att springa:

npm installera -g mocha

När det är klart skapar du en ny mapp för vårt projekt och kör följande inuti det:

npm installera chai

Detta kommer att installera en lokal kopia av Chai för vårt projekt. Skapa sedan en mapp som heter testa inuti vårt projekt katalog, eftersom det här är standardplatsen kommer Mocha leta efter tester.

Det är ganska mycket för installationen, nästa steg är att prata om hur du strukturerar dina appar när du följer en testdriven utvecklingsprocess.


Strukturera din app

Det är viktigt att veta, när man följer ett TDD-tillvägagångssätt, vad behöver ha test och vad som inte gör det. En tumregel är att inte skriva test för andra människor som redan testat kod. Vad jag menar med detta är följande: låt oss säga att din kod öppnar en fil, du behöver inte testa individen fs funktion, det är en del av languge och är antagligen redan väl testad. Detsamma gäller när du använder tredjepartsbibliotek, bör du inte strukturera funktioner som främst kallar dessa typer av funktioner. Du skriver inte riktigt test för dessa och på grund av detta har du luckor i TDD-cykeln.

Nu är det givetvis med varje programmeringsstil en hel del olika åsikter och människor kommer att ha olika åsikter om hur man TDD. Men det sätt jag använder är att du skapar enskilda komponenter som ska användas i din app, som alla löser ett unikt funktionellt problem. Dessa komponenter är byggda med hjälp av TDD så att de fungerar som förväntat och du kommer inte att bryta sitt API. Då skriver du ditt huvudskript, vilket är i princip all limkod, och behöver inte testas / kan inte testas, i vissa situationer.

Detta innebär också att de flesta av dina komponenter kan återanvändas i framtiden eftersom de inte har mycket att göra direkt med huvudskriptet.

Efter vad jag just sagt är det vanligt att skapa en mapp med namnet "lib"där du lägger alla enskilda komponenter. Så fram till denna punkt borde du ha Mcha och Chai installerat, och sedan en projektkatalog med två mappar: 'lib"och"testa'.


Komma igång med TDD

Bara om du är ny på TDD tyckte jag att det skulle vara en bra idé att snabbt täcka processen. Grundregeln är att du inte kan skriva någon kod om inte testlöparen berättar för dig.

I huvudsak skriver du vad din kod ska göra innan du faktiskt gör det. Du har ett riktigt fokuserat mål samtidigt som du kodar och du äventyrar aldrig din idé genom att bli sidospårad eller tänker för långt framåt. Förutom att eftersom hela din kod kommer att ha ett test anslutet till det kan du vara säker på att du aldrig kommer att bryta din app i framtiden.

Ett test är i själva verket bara en deklaration av vad en funktion förväntas göra när du kör, du kör sedan din testlöpare, vilket givetvis kommer att misslyckas (eftersom du inte har skrivit koden ännu) och då skriver du lägsta beloppet av kod som behövs för att klara det felaktiga testet. Det är viktigt att aldrig hoppa över det här steget, för ibland kommer ett test att passera redan innan du lägger till någon kod, på grund av annan kod som du har i samma klass eller funktion. När det händer skrev du antingen mer kod då du skulle för ett annat test eller det här är bara ett dåligt test (vanligtvis inte tillräckligt nog).

Återigen enligt vår regel ovan, om testet passerar genast kan du inte skriva någon kod, eftersom den inte berättade för dig. Genom att kontinuerligt skriva test och sedan implementera funktionerna konstruerar du fasta moduler som du kan lita på.

När du är klar med att implementera och testa din komponent kan du sedan gå tillbaka och refactor koden för att optimera den och städa upp den, men se till att refactoring inte misslyckas någon av de tester du har på plats och ännu viktigare, t lägg till några funktioner som inte testas.

Varje testbibliotek kommer att ha sin egen syntax, men de brukar följa samma mönster för att göra påståenden och sedan kontrollera om de passerar. Eftersom vi använder Mcha och Chai får vi ta en titt på båda deras syntaxer som börjar med Chai.


Mocha & Chai

Jag kommer att använda "Expect" BDD-syntaxen, för som jag nämnde kommer Chai med några alternativ ur lådan. Så här fungerar denna syntax, börjar du genom att ringa förväntade funktionen, överföra den objektet du vill göra ett påstående om och sedan kedja det med ett specifikt test. Ett exempel på vad jag menar kan vara enligt följande:

förväntar (4 + 5) .equal (9);

Det är den grundläggande syntaxen, vi säger att vi förväntar oss tillägget av 4 och 5 att lika 9. Nu är det här inte ett bra test eftersom 4 och 5 kommer att läggas till av Node.js innan funktionen ens kallas så vi testar i princip mina mattefärdigheter, men jag hoppas att du får den allmänna idén. Den andra sak du bör notera är att denna syntax inte är mycket läsbar, vad gäller flödet av en normal engelsk mening. Genom att veta detta lade Chai följande kedje getters som inte gör någonting, men du kan lägga till dem för att göra det mer verbalt och läsbart. Kedje getters är som följer:

  • till
  • vara
  • varit
  • är
  • den där
  • och
  • ha
  • med
  • av
  • samma
  • en
  • en

Med hjälp av ovanstående kan vi skriva om vårt tidigare test på något sådant:

förväntar (4 + 5) .to.equal (9);

Jag gillar verkligen hela bibliotekets känsla, som du kan kolla in i deras API. Enkla saker som att neka operationen är lika lätt som att skriva .inte före provet:

förväntar (4 + 5) .to.not.equal (10);

Så även om du aldrig har använt biblioteket tidigare kommer det inte vara svårt att ta reda på vad ett test försöker göra.

Det sista jag vill titta på innan vi går in i vårt första test är hur vi strukturerar vår kod i Mocka

Mocka

Mocka är testlöparen, så det bryr sig inte så mycket om de aktuella testerna, vad det bryr sig om är teststrukturen, för det är så det vet hur det går fel och hur man layoutar resultaten. Hur du bygger upp det skapar du flera beskriva block som skisserar de olika komponenterna i ditt bibliotek och sedan lägger du till Det blockerar för att specificera ett specifikt test.

För ett snabbt exempel, låt oss säga att vi hade en JSON-klass och den klassen hade en funktion att analysera JSON och vi ville se till att parsfunktionen kan upptäcka en dåligt formaterad JSON-sträng. Vi kunde strukturera detta så här:

beskriva ("JSON", funktion () beskriv ("parse ()", funktion () det ska ("ska upptäcka felaktiga JSON-strängar", funktion () // Test går här););) ;

Det är inte komplicerat, och det handlar om 80% personlig preferens, men om du behåller den här typen av format, ska testresultaten komma ut i ett mycket läsbart format.

Vi är nu redo att skriva vårt första bibliotek, låt oss börja med en enkel synkronmodul, för att vi ska bli bättre bekanta med systemet. Vår app måste kunna acceptera kommandoradsalternativ för att ställa in saker som hur många nivåer av mappar vår app ska söka igenom och själva frågan.

För att ta hand om allt detta skapar vi en modul som accepterar kommandos sträng och analyserar alla inkluderade alternativ tillsammans med deras värden.

Taggmodulen

Det här är ett bra exempel på en modul som du kan återanvända i alla dina kommandoradsappar, eftersom det här problemet uppstår mycket. Detta kommer att bli en förenklad version av ett faktiskt paket som jag har på npm heter ClTags. Så för att komma igång, skapa en fil med namnet tags.js inuti lib-mappen och sedan en annan fil med namnet tagsSpec.js inuti testmappen.

Vi måste dra in Chai-förväntade funktionen, eftersom det kommer att vara den syntaks som vi ska använda och vi måste dra in själva taggarfilen så att vi kan testa den. Sammantaget med en viss inledande inställning ska det se ut så här:

varför förvänta = kräva ("chai"). var tags = kräver ("... / lib/tags.js"); beskriv ("Taggar", funktion () );

Om du kör kommandot "mocha" nu från rotet av vårt projekt, ska allt passera som förväntat. Låt oss nu tänka på vad vår modul ska göra; vi vill skicka det kommandoradsargumentet som användes för att köra appen, och då vill vi att det ska bygga ett objekt med alla taggar, och det skulle vara trevligt om vi också kunde skicka det till ett standardobjekt med inställningar, så om inget får överträffas, vi kommer att ha några inställningar som redan lagrats.

När det gäller taggar ger många apper även genvägar som bara är ett tecken, så låt oss säga att vi ville ställa in djupet på vår sökning, vi skulle kunna låta användaren antingen ange något som --Djup = 2 eller något liknande -d = 2 vilket borde ha samma effekt.

Så låt oss bara börja med de långformade taggarna (till exempel '--depth = 2'). Till att börja med, låt oss skriva det första testet:

beskriva ("Taggar", funktion () beskriv ("# parse ()", funktion () det ("ska tolka långformade taggar", funktion () var args = ["--depth = 4" --hello = world "]; var results = tags.parse (args); expect (results) .to.have.a.property (" depth ", 4); expect (results) .to.have.a.property ("Hej världen"); ); ); );

Vi lade till en metod i vår testpaket som heter parse och vi lade till ett test för långformade taggar. Inuti detta test skapade jag ett exempelkommando och lade till två påståenden för de två egenskaper som den skulle hämta.

Running Mocka nu borde du få ett fel, nämligen det taggar har inte en parse fungera. Så för att åtgärda detta fel låt oss lägga till en parse funktionen till taggen modulen. Ett ganska typiskt sätt att skapa en nodmodul är som så:

export = module.exports = ; exports.parse = function () 

Felet sa att vi behövde a parse metod så vi skapade det, vi lade inte till någon annan kod inuti, eftersom den ännu inte berättade för oss. Genom att hålla fast vid det minsta minimumet är du säker på att du inte skriver mer än du ska och slutar med otestad kod.

Låt oss nu springa Mocha igen, den här gången borde vi få ett fel som säger att det inte kan läsa en egenskap som heter djup från en odefinierad variabel. Det beror på att vi idag parse funktionen returnerar ingenting, så låt oss lägga till en kod så att den kommer att returnera ett objekt:

exports.parse = function () var options =  återställningsalternativ; 

Vi rör dig långsamt, om du kör Mcha igen, borde de inte vara några undantag som kastas, bara ett rent felmeddelande som säger att vårt tomma objekt inte har någon egendom som heter djup.


Nu kan vi komma in i någon riktig kod. För vår funktion att analysera taggen och lägga till den i vårt objekt måste vi cykla genom argumentmatrisen och ta bort dubbla bindestreck vid början av nyckeln.

exports.parse = function (args) var options =  för (var jag i args) // Cykla genom args var arg = args [i]; // Kontrollera om Långformad tagg om (arg.substr (0, 2) === "-") arg = arg.substr (2); // Kolla för lika tecken om (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nyckel = arg.shift (); alternativ [nyckel] = arg.join ("=");  returalternativ; 

Denna kod cyklar genom listan över argument, ser till att vi har att göra med en långformad tagg och splittrar sedan den med den första lika karaktären för att skapa nyckel- och värdeparet för alternativobjektet.

Nu löser detta oss nästan problemet, men om vi kör Mcha igen ser vi att vi nu har en nyckel för djupet, men det är satt till en sträng istället för ett nummer. Nummer är lite enklare att arbeta med senare i vår app, så nästa kod måste vi lägga till är att konvertera värden till nummer när det är möjligt. Detta kan uppnås med vissa RegEx och parseInt fungerar som följer:

 om (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nyckel = arg.shift (); varvärde = arg.join ("="); om (/^[0-9]+$/.test(value)) value = parseInt (värde, 10);  alternativ [tangent] = värde; 

Köra Mocka nu borde du få ett godkännande med ett test. Nummeromvandlingen borde förmodligen vara i sitt eget test, eller åtminstone nämnt i testdeklarationen, så att du av misstag inte tar bort talomvandlingsbeviset. så bara add-on "add and convert numbers" till Det deklaration för detta test eller separera det i en ny Det blockera. Det beror verkligen om du anser att detta "uppenbara standardbeteende" eller en separat funktion.


Nu som jag har försökt att stressa hela denna artikel, när du ser en passande spec, är det dags att skriva fler test. Nästa sak som jag ville lägga till var standardmatrisen, så inuti tagsSpec filen lägger till följande Det blockera direkt efter den föregående:

 det ("ska tolka långformade taggar och konvertera nummer", funktion () var args = ["--depth = 4", "--hello = world"]; var results = tags.parse (args); expect resultat) .to.have.a.property ("depth", 4); expect (results) .to.have.a.property ("hej", "world");); den ("bör återgå till standardinställningar", funktion () var args = ["--depth = 4", "--hello = world"]; var standard = djup: 2, foo: "bar"; var results = tags.parse (args, default); var expected = djup: 4, foo: "bar", hej: "world"; expect (results) .to.deep.equal (expected););

Här använder vi ett nytt test, den djupa lika som är bra för att matcha två objekt för lika värde. Alternativt kan du använda eql test som är en genväg men jag tycker att det här är tydligare. Detta test passerar två argument som kommandosträngen och skickar två standardvärden med en överlappning, så att vi kan få en bra spridning på testfallen.

Running Mocka nu borde du få en sorts diff, som innehåller skillnaderna mellan vad som förväntas och vad det faktiskt fick.


Låt oss nu fortsätta tillbaka till tags.js modulen, och låt oss lägga till den här funktionaliteten. Det är en ganska enkel fix att lägga till, vi behöver bara acceptera den andra parametern och när den är inställd på ett objekt kan vi ersätta det normala tomma objektet i början med det här objektet:

exports.parse = funktion (args, standardvärden) var options = ; om (typ av standardvärden === "objekt" &&! (standard instans av Array)) options = default

Detta kommer att föra oss tillbaka till en grön stat. Nästa sak jag vill lägga till är möjligheten att bara ange en tagg utan ett värde och låt det fungera som en booleska. Till exempel, om vi bara ställa in --searchContents eller något sådant, kommer det bara att lägga till det till vårt alternativ array med ett värde av Sann.

Testet för detta skulle se ut som följande:

 den ("ska acceptera taggar utan värden som en bool", funktion () var args = ["--searchContents"]; var results = tags.parse (args); expect (results) .to.have.a.property ("searchContents", true););

Om du kör detta kommer vi att ge oss följande fel som tidigare:


Inne i för slinga, när vi fick en matchning för en långformad tag, kontrollerade vi om det innehöll en lika tecken; Vi kan snabbt skriva koden för detta test genom att lägga till en annan klausul till det om uttalande och justering av värdet till Sann:

 om (arg.indexOf ("=")! == -1) arg = arg.split ("="); var nyckel = arg.shift (); varvärde = arg.join ("="); om (/^[0-9]+$/.test(value)) value = parseInt (värde, 10);  alternativ [tangent] = värde;  annat alternativ [arg] = true; 

Nästa sak som jag vill lägga till är substitutionerna för de korta taggarna. Detta blir den tredje parametern till parse funktionen och kommer i grunden att vara ett objekt med bokstäver och motsvarande ersättningar. Här är specifikationen för detta tillägg:

 den ("bör acceptera kortformade taggar", funktion () var args = ["-sd = 4", "-h"]; var ersättning = s: "searchContents", d: "depth" hej "; var results = tags.parse (args, , ersättningar); var expected = searchContents: true, depth: 4, hej: true; expect (results) .to.deep.equal (expected); );

Problemet med shorthand-taggar är att de kan kombineras i rad. Vad jag menar med detta är till skillnad från de länge bildade taggarna där var och en är separat, med korta handtag - eftersom de bara är ett brev långt - du kan ringa tre olika genom att skriva -VGH. Detta gör det lite svårare att parsa eftersom vi fortfarande måste tillåta samma operatör för att du ska lägga till ett värde för den senast angivna taggen samtidigt som du fortfarande måste registrera de andra taggarna. Men oroa dig inte, det är inget som inte kan lösas med tillräckligt popping och skiftande.

Här är hela fixen, från början av parse fungera:

exports.parse = funktion (args, standard, ersättningar) var options = ; om (typ av standardvärden === "objekt" &&! (standard instans av Array)) alternativ = standardvärden om (typ av ersättningar === "objekt" &&! (standard instans av Array)) för (var jag i args)  var arg = args [i]; om (arg.charAt (0) === "-" && arg.charAt (1)! = "-") arg = arg.substr (1); om (arg.indexOf ("=")! == -1) arg = arg.split ("="); var keys = arg.shift (); varvärde = arg.join ("="); arg = keys.split (""); var nyckel = arg.pop (); om (ersättare.hasOwnProperty (nyckel)) nyckel = ersättningar [tangent];  args.push ("-" + key + "=" + värde);  annars arg = arg.split ("");  arg.forEach (funktion (nyckel) om (ersättare.hasOwnProperty (nyckel)) nyckel = ersättningar [tangent]; args.push ("-" + -tangenten);); 

Det är mycket kod (i jämförelse) men allt vi verkligen gör är att dela upp argumentet med ett lika tecken och splittra den nyckeln i de enskilda bokstäverna. Så till exempel om vi passerade -gj = asd vi skulle dela upp asd till en variabel som heter värde, och då skulle vi dela upp gj avsnitt i enskilda tecken. Den sista karaktären (j i vårt exempel) blir nyckeln för värdet (asd) medan alla andra bokstäver framför den kommer bara att läggas till som vanliga booleska taggar. Jag ville inte bara bearbeta dessa taggar nu, bara om vi ändrade implementeringen senare. Så vad vi gör är att bara konvertera dessa korta handtag till den långa versionen och sedan låta vårt skript hantera det senare.

Running Mocha igen tar oss tillbaka till våra strålande gröna resultat från fyra tester som passerar för denna modul.

Nu finns det några fler saker vi kan lägga till i den här taggenmodulen för att göra det närmare paketet npm, till exempel möjligheten att också lagra vanliga textargument för saker som kommandon eller förmågan att samla all text i slutet för en förfrågningsegenskap. Men den här artikeln börjar snart bli lång och jag vill fortsätta att implementera sökfunktionen.


Sökmodulen

Vi gick bara igenom att skapa en modul steg för steg efter ett TDD-tillvägagångssätt och jag hoppas att du fick idén och känslan av hur du skriver så här. Men för att hålla denna artikel i rörelse, för resten av artikeln, kommer jag att påskynda testprocessen genom att gruppera saker och visa dig bara de sista versionerna av tester. Det är mer av en guide till olika situationer som kan komma upp och hur man skriver tester för dem.

Så bara skapa en fil med namnet search.js inuti lib-mappen och a searchSpec.js filen inuti testmappen.

Öppna sedan specfilen och låt oss konfigurera vårt första test som kan vara för funktionen att få en lista med filer baserade på a djup parameter, det här är också ett bra exempel på test som kräver lite extern inställning för att de ska kunna fungera. När du arbetar med externa objektliknande data eller i våra fallfiler vill du ha en fördefinierad inställning som du vet kommer att fungera med dina test, men du vill inte heller lägga till falsk info till ditt system.

Det finns i grund och botten två alternativ för att lösa detta problem, du kan antingen mocka data, som jag nämnde ovan om du hanterar språkens egna kommandon för att ladda data, behöver du inte nödvändigtvis testa dem. I sådana fall kan du helt enkelt tillhandahålla "hämtade" data och fortsätta med testningen, som om vi gjorde med kommandosträngen i taggar biblioteket. Men i det här fallet testar vi den rekursiva funktionaliteten vi lägger till i språkfilens läsfunktioner, beroende på det angivna djupet. I sådana fall behöver du skriva ett test och så måste vi skapa några demofiler för att testa filen. Alternativet är att kanske stubba fs funktioner att bara springa men inte göra någonting, och då kan vi räkna hur många gånger vår falska funktion sprang eller något sådant (kolla spioner) men för vårt exempel kommer jag bara att skapa några filer.

Mocka ger funktioner som kan köras både före och efter dina tester, så att du kan utföra denna typ av extern inställning och städning runt dina test.

För vårt exempel kommer vi att skapa ett par testfiler och mappar på två olika djup så att vi kan testa den funktionen:

varför förvänta = kräva ("chai"). var search = kräver ("... / lib/search.js"); var fs = kräver ("fs"); beskriva ("Sök", funktion () beskriv ("# scan ()", funktion () före (funktion ) om ! 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 ";" ");); fs.unlinkSync (". test_files / dir / c"); fs.rmdirSync (". test_files / dir"); "); fs.unlinkSync (". test_files / a "); fs.unlinkSync (". test_files / b "), fs.rmdirSync (". test_files "););););

Dessa kommer att kallas baserat på beskriva blockera de är i, och du kan även köra kod före och efter varje Det blockera med beforeEach eller afterEach istället. Funktionerna själva använder bara standardnodkommandon för att skapa respektive ta bort filerna. Nästa måste vi skriva det faktiska testet. Detta bör gå bredvid efter funktion, fortfarande inne i beskriva blockera:

 den ("ska hämta filerna från en katalog", funktion (gjort) search.scan (". test_files", 0, funktion (err, flist) expect (flist) .to.deep.equal ([".test_files / a "," .test_files / b "," testa_files / dir / c "," .test_files / dir2 / d "]); gjort ();););

Detta är vårt första exempel på att testa en async-funktion, men som du kan se är det lika enkelt som tidigare; allt vi behöver göra är att använda Gjort funktion Mocha ger i Det förklaringar att berätta när vi är färdiga med detta test.

Mocka kommer automatiskt att upptäcka om du angav Gjort variabel i återuppringningen och det väntar på att det kallas så att du kan testa asynkron kod verkligen enkelt. Det är också värt att nämna att det här mönstret är tillgängligt under Mocha, du kan till exempel använda det här i innan eller efter Fungerar om du behöver konfigurera något asynkront.

Nästa vill jag skriva ett test som gör att djupparametern fungerar om den är inställd:

 den ("bör stoppa vid ett visst djup", funktion (gjort) search.scan (". test_files", 1, funktion (err, flist) expect (flist) .to.deep.equal ([".test_files / en "," .test_files / b ",]); gjort ();););

Inget annat här, bara ett annat vanligt test. Om du kör detta i Mocka får du ett fel att sökningen inte har några metoder, i grund och botten eftersom vi inte har skrivit något i det. Så låt oss lägga till en översikt med funktionen:

var fs = kräver ("fs"); export = module.exports = ; exports.scan = funktion (dir, djup, gjort) 

Om du nu kör Mocha igen, kommer det pausa och väntar på att denna async-funktion kommer att återvända, men eftersom vi inte har ringt tillbaka till återuppringningen kommer testet bara att vara timeout. Som standard bör det vara dags efter ca två sekunder, men du kan justera detta med hjälp av this.timeout (millisekunder) Inne i ett beskriv eller blockera, för att justera sina tidsintervaller.

Den här skanningsfunktionen ska ta en väg och ett djup och återge en lista över alla filer som den hittar. Det här är faktiskt typiskt knepigt när du börjar tänka på hur vi i huvudsak återskapar två olika funktioner tillsammans i en enda funktion. Vi måste rekrytera genom de olika mapparna och sedan måste dessa mappar skanna själva och bestämma sig för att gå vidare.

Att göra det här synkront är bra eftersom du kan gå igenom det en efter en, långsamt slutföra en nivå eller väg åt gången. När man arbetar med en async-version blir det lite mer komplicerat eftersom du inte bara kan göra en för varje loop eller något, eftersom det inte kommer att pausa mellan mappar, kommer de alla väsentligen att springa samtidigt som varje återvändande olika värden och de skulle sortera över varandra.

Så för att få det att fungera måste du skapa en sorts stack där du kan asynkront behandla en åt gången (eller alla omedelbart om du använder en kö istället) och sedan behålla någon order på det sättet. Det är en mycket specifik algoritm så jag håller bara ett fragment av Christopher Jeffrey som du kan hitta på Stack Overflow. Det gäller inte bara för att ladda filer, men jag har använt det i ett antal applikationer, i princip allt där du behöver bearbeta en rad objekt en och en med asyncfunktioner.

Vi behöver ändra det lite, för vi vill ha ett djupalternativ, hur djupalternativet fungerar, är du inställd på hur många nivåer av mappar du vill kontrollera, eller noll för att återfå obestämd tid.

Här är den färdiga funktionen med hjälp av snippet:

exports.scan = funktion (dir, djup, gjort) deep--; var results = []; fs.readdir (dir, funktion (err, lista) om (err) returnera gjort (err); var i = 0; (funktion nästa () var file = list [i ++] null, resultat); fil = dir + '/' + fil; fs.stat (fil, funktion (err, stat) if (stat && stat.isDirectory ()) om (djup! == 0) var ndepth = djup> 1) djup-1: 1; exports.scan (fil, ndepth, funktion (err, res) results = results.concat (res); next ();); annat nästa ; else results.push (fil); nästa (););) ();); ;

Mocka ska nu passera båda proven. Den sista funktionen vi behöver genomföra är den som kommer att acceptera en rad olika sökvägar och ett sökord och returnera alla matchningar. Här är testet för det:

 beskriv "(match # js "]; var results = search.match (". js ", filer); förvänta (resultat) .to.deep.equal ([" world.js "," another.js "]); results = search.match ("hej", filer); förvänta (resultat) .to.deep.equal (["hello.txt"]);););

Och sist men inte minst, låt oss lägga till funktionen till search.js:

export.match = funktion (fråga, filer) var matchar = []; files.forEach (funktion (namn) if (name.indexOf (query)! == -1) matches.push (namn);); retur matcher; 

Bara för att vara säker på att du kör Mcha igen, borde du ha totalt sju tester som passerar.



Få alltid att falla på plats

Det sista steget är att verkligen skriva limkoden som drar alla våra moduler tillsammans. så i roten till vårt projekt lägg till en fil med namnet app.js eller något liknande och lägg till följande inuti:

#! / usr / bin / env node var tags = kräver ("./ lib / tags.js"); var search = kräver ("./ lib / search.js"); var standard = sökväg: ".", fråga: "", djup: 2 varbyte = p: "sökväg", q: "fråga", d: "djup", h: "hjälp" taggar = taggar .parse (process.argv, standardinställningar, ersättningar); om (tags.help) console.log ("Användning: ./app.js -q = query [-d = depth] [-p = path]");  else search.scan (tags.path, tags.depth, funktion (err, filer) search.match (tags.query, files) .forEach (funktion (fil) console.log (file);); ); 

Ingen faktisk logik pågår här, vi kopplar bara ihop de olika modulerna ihop för att få de önskade resultaten. Jag brukar inte testa den här koden eftersom det bara är limkod som redan har testats.

Du kan nu göra ditt skript körbart (chmod + x app.js på ett Unix-system) och kör sedan det som så:

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

Eventuellt anpassning av några av de andra platsinnehavarna vi installerar.



Slutsats

I den här artikeln har vi byggt en hel filsökningsapp, om än en enkel, men jag tror att den visar processen som helhet ganska bra.

Några personliga råd framåt; Om du ska göra mycket TDD, ställ in din miljö. Många av den tid som personerna associerar med TDD beror på att de måste fortsätta byta fönster runt, öppna och stänga olika filer, sedan springa test och upprepa detta 80 dussin gånger om dagen. I sådant fall stör det arbetsflödet som minskar produktiviteten.