Testdriven utveckling är en programmeringspraxis som har predikats och främjats av varje utvecklingssamhälle på planeten. Och ändå är det en rutin som i stor utsträckning försummas av en utvecklare samtidigt som man lär sig en ny ram. Skriva enhetstester från dag ett hjälper dig att skriva bättre kod, upptäcka buggar med lätthet och upprätthålla ett bättre utvecklingsflöde.
Angular, som är en fullfjädrad utvecklingsplattform för framsidan, har sin egen uppsättning verktyg för testning. Vi använder följande verktyg i denna handledning:
det ("ska ha en definierad komponent", () => förvänta (komponent) .toBeDefined (););
Testbed
och ComponentFixtures
och hjälparfunktioner som async
och fakeAsync
är en del av @ Vinkel / kärna / testning
paket. Att bekanta sig med dessa verktyg är nödvändigt om du vill skriva tester som visar hur dina komponenter interagerar med sin egen mall, tjänster och andra komponenter.Vi kommer inte att täcka funktionella tester med hjälp av Protractor i denna handledning. Protractor är en populär end-to-end testram som samverkar med programmets användargränssnitt med en faktisk webbläsare.
I den här handledningen är vi mer oroade över provning av komponenter och komponentens logik. Vi kommer emellertid att skriva ett par tester som visar grundläggande UI-interaktion med jasminramen.
Målet med denna handledning är att skapa fronten för en Pastebin-applikation i en testdriven utvecklingsmiljö. I denna handledning följer vi den populära TDD-mantran, som är "röd / grön / refaktor". Vi kommer att skriva tester som initialt misslyckas (röd) och sedan arbeta med vår ansökningskod för att få dem att passera (grön). Vi ska refactor vår kod när det börjar stinka, vilket betyder att det blir uppblåst och ful.
Vi ska skriva tester för komponenter, deras mallar, tjänster och pastebin-klassen. Bilden nedan illustrerar strukturen i vår Pastebin-applikation. De punkter som är gråtonade kommer att diskuteras i den andra delen av handledningen.
I serieens första del kommer vi enbart att koncentrera oss på att inrätta testmiljön och skriva grundläggande tester för komponenter. Vinkel är en komponentbaserad ram; Därför är det en bra idé att spendera lite tid att lära känna skrivprov för komponenter. I den andra delen av serien kommer vi att skriva mer komplexa tester för komponenter, komponenter med ingångar, dirigerade komponenter och tjänster. I slutet av serien kommer vi att ha en fullt fungerande Pastebin applikation som ser ut så här.
Vy över Pastebin-komponentenVisa på AddPaste-komponentenI denna handledning lär du dig att:
Hela koden för handledningen finns på Github.
https://github.com/blizzerand/pastebin-angular
Klon repo och känn dig fri att kolla koden om du är i tvivel på något stadium av denna handledning. Låt oss börja!
Utvecklarna hos Angular har gjort det enkelt för oss att ställa in vår testmiljö. För att komma igång måste vi installera Angular först. Jag föredrar att använda Angular-CLI. Det är en allt-i-ett-lösning som tar hand om att skapa, generera, bygga och testa ditt Angular-projekt.
ng ny Pastebin
Här är katalogstrukturen skapad av Angular-CLI.
Eftersom våra intressen är benägna mer mot testaspekterna i Angular, måste vi se upp för två typer av filer.
karma.conf.js är konfigurationsfilen för Karma-testlöparen och den enda konfigurationsfilen som vi behöver för att skriva enhetstester i Angular. Som standard är Chrome den vanliga webbläsare som används av Karma för att fånga prov. Vi kommer att skapa en anpassad launcher för att köra huvudlösa Chrome och lägga till den i webbläsare
array.
/*karma.conf.js*/ webbläsare: ['Chrome', 'ChromeNoSandboxHeadless'], customLaunchers: ChromeNoSandboxHeadless: bas: 'Chrome', flaggor: ['- ingen sandlåda', // Se https: / /chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md '--headless', '--disable-gpu', // Utan en fjärranslutningsfelsökning avgår Google Chrome omedelbart. '--remote-debugging-port = 9222',],,,
Den andra typen av fil som vi behöver ta hand om är allting som slutar med .spec.ts
. Enligt konventionen kallas testen som skrivs i Jasmine specs. Alla testspecifikationer bör placeras inuti applikationens src / app /
katalog eftersom det är där Karma letar efter testspecifikationerna. Om du skapar en ny komponent eller en tjänst är det viktigt att du placerar dina testspecifikationer i samma katalog som koden för komponenten eller tjänsten finns i.
De ng ny
kommandot har skapat en app.component.spec.ts
filen för vår app.component.ts
. Känn dig fri att öppna den och titta bra på Jasmintesterna i Angular. Även om koden inte har någon mening är det bra. Vi kommer att behålla AppComponent som det är för nu och använda det för att vara värd för ruttarna vid någon senare punkt i handledningen.
Vi behöver en Pastebin-klass för att modellera vår Pastebin inom komponenterna och testen. Du kan skapa en med Angular-CLI.
ng generera klass Pastebin
Lägg till följande logik på Pastebin.ts:
export klass Pastebin id: nummer; titel: sträng; språk: sträng; klistra in: sträng; konstruktör (värden: Objekt = ) Object.assign (detta, värden); export const Språk = ["Ruby", "Java", "JavaScript", "C", "Cpp"];
Vi har definierat en Pastebin-klass, och varje förekomst av denna klass kommer att ha följande egenskaper:
id
titel
språk
klistra
Skapa en annan fil som heter pastebin.spec.ts
för testpaketet.
/ * pastebin.spec.ts * / // Importera Pastebin-klassens import Pastebin från './pastebin'; Beskriv ('Pastebin', () => it ('ska skapa en förekomst av Pastebin', () => förvänta (ny Pastebin ()). toBeTruthy (););)
Testpaketet börjar med a beskriva
block, vilket är en global jasminfunktion som accepterar två parametrar. Den första parametern är titeln på testpaketet och den andra är den faktiska implementeringen. Specifikationerna definieras med hjälp av en Det
funktion som tar två parametrar, som liknar den för beskriva
blockera.
Flera specifikationer (Det
block) kan nästas inuti en test svit (beskriva
blockera). Se till att testpaketstitlarna heter så att de är otvetydiga och mer läsbara eftersom de är avsedda att fungera som dokumentation för läsaren.
Förväntningar, implementerade med hjälp av förvänta
funktion, används av Jasmine för att avgöra om en spec ska passera eller misslyckas. De förvänta
funktionen tar en parameter som är känd som det verkliga värdet. Den är sedan kedjad med en annan funktion som tar det förväntade värdet. Dessa funktioner kallas matcharfunktioner, och vi kommer att använda matchfunktionerna som toBeTruthy ()
, att definieras()
, att vara()
, och att innehålla()
mycket i denna handledning.
förvänta sig (ny Pastebin ()). toBeTruthy ();
Så med denna kod har vi skapat en ny instans av Pastebin-klassen och förväntar oss att den är sann. Låt oss lägga till en annan specifikation för att bekräfta att pastebinmodellen fungerar som avsedd.
den ("ska acceptera värden", () => låt pastebin = ny Pastebin (); pastebin = id: 111, titel: "Hej värld", språk: "Ruby" förvänta (pastebin.id) .toEqual (111); expect (pastebin.language) .toEqual ("Ruby"); förvänta (pastebin.paste) .toEqual ('print' Hello ''););
Vi har instanser Pastebin-klassen och lagt till några förväntningar på vår testspec. Springa ng test
för att verifiera att alla tester är gröna.
Generera en tjänst med kommandot nedan.
ng generera service pastebin
PastebinService
kommer att vara värd för logiken för att skicka HTTP-förfrågningar till servern; Vi har dock inte ett server API för den applikation vi bygger. Därför ska vi simulera serverkommunikationen med hjälp av en modul som kallas InMemoryWebApiModule.
Installera vinkel-i-minne-web-api
via npm:
npm installera vinkel-i-minne-web-api -save
Uppdatera AppModule med den här versionen.
/ * app.module.ts * / import BrowserModule från '@ vinkel / plattform-webbläsare'; importera NgModule från '@ vinkel / kärna'; // Komponenter importerar AppComponent från './app.component'; // Service för pastebinimport PastebinService från "./pastebin.service"; // Moduler som används i denna handledning import HttpModule från '@ vinkel / http'; // I minnet Web api för att simulera en http-server import InMemoryWebApiModule från "angular-in-memory-web-api"; importera InMemoryDataService från './in-memory-data.service'; @NgModule (declarations: [AppComponent,], import: [BrowserModule, HttpModule, InMemoryWebApiModule.forRoot (InMemoryDataService),], leverantörer: [PastebinService], bootstrap: [AppComponent]) exportklass AppModule
Skapa en InMemoryDataService
som implementerar InMemoryDbService
.
/*in-memory-data.service.ts*/ importera InMemoryDbService från "angular-in-memory-web-api"; importera Pastebin från './pastebin'; export klass InMemoryDataService implementerar InMemoryDbService createDb () const pastebin: Pastebin [] = [id: 0, titel: "Hej världen Ruby", språk: "Ruby", klistra in: "sätter" Hello World " : 1, titel: "Hej värld C", språk: "C", klistra in: 'printf ("Hej värld");', id: 2, titel: "Hej världens CPP", språk: "C ++" klistra in: 'cout<<"Hello world";', id: 3, title: "Hello world Javascript", language: "JavaScript", paste: 'console.log("Hello world")' ]; return pastebin;
Här, pastebin
är en rad provpasta som kommer att returneras eller uppdateras när vi utför en HTTP-åtgärd som http.get
eller http.post
.
/*pastebin.service.ts * / import Injectable från '@ vinkel / kärna'; importera Pastebin från './pastebin'; importera Http, Headers från '@ vinkel / http'; importera 'rxjs / add / operator / toPromise'; @Injectable () exportklass PastebinService // Projektet använder InMemoryWebApi för att hantera Server API. // Här simulerar "api / pastebin" ett server API-url privat pastebinUrl = "api / pastebin"; privata rubriker = nya rubriker ('Content-Type': "application / json"); konstruktör (privat http: Http) // getPastebin () utför http.get () och returnerar ett löfte public getPastebin (): Promisereturn this.http.get (this.pastebinUrl) .toPromise () .then (response => response.json (). data) .catch (this.handleError); privathandtagError (fel: någon): Promise console.error ("Ett fel inträffade", fel); returnera Promise.reject (error.message || fel);
De getPastebin ()
Metoden gör en HTTP.get-förfrågan och returnerar ett löfte som löser en rad Pastebin-objekt som returneras av servern.
Om du får en Ingen leverantör för HTTP fel När du kör en spec måste du importera HTTPModulen till den aktuella specfilen.
Komponenter är det mest grundläggande byggstenen i ett användargränssnitt i en vinkelapplikation. En vinkelapplikation är ett träd av vinklade komponenter.
- Angular Documentation
Som framhävt tidigare i avsnittet Översikt arbetar vi med två komponenter i denna handledning: PastebinComponent
och AddPasteComponent
. Pastebin-komponenten består av en tabellstruktur som listar all pasta som hämtats från servern. AddPaste-komponenten innehåller logiken för att skapa nya pastor.
Gå vidare och generera komponenterna med Angular-CLI.
ng g komponent - spec = falskt pastebin
De --spec = false
alternativet säger att Angular-CLI inte ska skapa en specfil. Detta beror på att vi vill skriva enhetsprov för komponenter från början. Skapa en pastebin.component.spec.ts
filen inuti pastebin-komponent mapp.
Här är koden för pastebin.component.spec.ts
.
importera TestBed, ComponentFixture, async från '@ vinkel / kärna / testning'; importera DebugElement från "@ vinkel / kärna"; importera PastebinComponent från '. /pastebin.component'; importera By från '@ vinkel / plattform-webbläsare'; importera Pastebin, Languages från '... / pastebin'; // Moduler som används för testning av import HttpModule från '@ vinkel / http'; beskriv ('PastebinComponent', () => // Typescript deklarationer. låt komp .: PastebinComponent; Låt fixture: ComponentFixture; låt de: DebugElement; låt element: HTMLElement; låt mockPaste: Pastebin []; // beforeEach kallas en gång före varje "det" block i ett test. // Använd detta för att konfigurera till komponenten, injicera tjänster etc. föreEach (() => TestBed.configureTestingModule (deklarationer: [PastebinComponent], // förklara importen av testkomponenten: [HttpModule],); fixture = TestBed .createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('. pastebin')); element = de.nativeElement;); )
Det händer mycket här. Låt oss bryta upp det och ta en bit åt gången. Inom beskriva
blockera, vi har förklarat några variabler, och sedan har vi använt a beforeEach
fungera. beforeEach ()
är en global funktion som tillhandahålls av Jasmine och, som namnet antyder, det påbereds en gång före varje spec i beskriva
block där det kallas.
TestBed.configureTestingModule (deklarationer: [PastebinComponent], // förklara importen av testkomponenten: [HttpModule],);
Testbed
klassen är en del av verktygen Angular testing, och det skapar en testmodul liknande den hos @NgModule
klass. Dessutom kan du konfigurera Testbed
använda configureTestingModule
metod. Du kan till exempel skapa en testmiljö för ditt projekt som emulerar den faktiska Angular-applikationen och du kan sedan dra en komponent från din applikationsmodul och fästa den igen på den här testmodulen.
fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('div')); element = de.nativeElement;
Från Angular dokumentationen:
Decreate
Metoden returnerar aComponentFixture
, ett handtag på testmiljön som omger den skapade komponenten. Fixturen ger tillgång till komponentinstansen själv och tillDebugElement
, vilket är ett handtag på komponentens DOM-element.
Som nämnts ovan har vi skapat en fixtur av PastebinComponent
och sedan använde den fixturen för att skapa en förekomst av komponenten. Vi kan nu komma åt komponentens egenskaper och metoder inom våra test genom att ringa comp.property_name
. Eftersom fixturen också ger tillgång till debugElement
, Vi kan nu fråga om DOM-element och väljare.
Det finns ett problem med vår kod som vi ännu inte har tänkt på. Vår komponent har en extern mall och en CSS-fil. Att hämta och läsa dem från filsystemet är en asynkron aktivitet, till skillnad från resten av koden, som är helt synkron.
Angular erbjuder dig en funktion som heter async ()
som tar hand om alla asynkrona saker. Vad async gör är att hålla reda på alla asynkrona uppgifter inuti den, samtidigt som du döljer komplexiteten i asynkron utförande från oss. Så vi kommer nu ha två före varje funktion, en asynkron beforeEach ()
och en synkron beforeEach ()
.
/ * pastebin.component.spec.ts * / // beforeEach kallas en gång före varje "det" -blocket i ett test. // Använd det här för att konfigurera till komponenten, injicera tjänster etc. föreEach (async (() => // async före används för att sammanställa externa mallar som är vilken asynkaktivitet TestBed.configureTestingModule (deklarationer: [PastebinComponent], / / deklarera testkomponentimporten: [HttpModule],) .compileComponents (); // kompilera mall och css)); beforeEach (() => // Och här är synkron async funktion fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('pastebin')); element = de.nativeElement;);
Vi har inte skrivit några testspecifikationer ännu. Det är dock en bra idé att skapa en översikt av specifikationerna i förväg. Bilden nedan visar en grov design av Pastebin-komponenten.
Vi måste skriva prov med följande förväntningar.
pastebinService
injiceras i komponenten, och dess metoder är tillgängliga.onInit ()
kallas.De första tre testerna är lätta att genomföra.
det ("ska ha en komponent", () => förvänta (comp) .toBeTruthy ();); det ("ska ha en titel", () => comp.title = 'Pastebin Application'; fixture.detectChanges (); expect (element.textContent) .toContain (comp.title);) en tabell för att visa pastorna ', () => expect (element.innerHTML) .toContain ("thead"); förvänta (element.innerHTML) .toContain ("tbody");)
I en testmiljö binder inte Angular automatiskt komponentens egenskaper med mallelementen. Du måste uttryckligen ringa fixture.detectChanges ()
varje gång du vill binda en komponentegenskap med mallen. Att köra testet bör ge dig ett fel eftersom vi ännu inte har deklarerat egenskapen för titeln i vår komponent.
titel: string = "Pastebin Application";
Glöm inte att uppdatera mallen med en grundläggande tabellstruktur.
titel
id Titel Språk Koda
När det gäller resten av fallen måste vi injicera Pastebinservice
och skriv test som handlar om komponent-service interaktion. En riktig tjänst kan ringa till en fjärrserver, och sprutning av den i sin råa form kommer att vara en mödosam och utmanande uppgift.
I stället bör vi skriva test som fokuserar på huruvida komponenten interagerar med tjänsten som förväntat. Vi ska lägga till specs som spionerar på pastebinService
och dess getPastebin ()
metod.
Först importera PastebinService
in i vårt testpaket.
importera PastebinService från "... /pastebin.service";
Lägg sedan till den i leverantörer
array inuti TestBed.configureTestingModule ()
.
TestBed.configureTestingModule (declarations: [CreateSnippetComponent], leverantörer: [PastebinService],);
Koden nedan skapar en jaspinspion som är utformad för att spåra alla samtal till getPastebin ()
metod och returnera ett löfte som omedelbart löser till mockPaste
.
// Den verkliga PastebinService injiceras i komponenten låt pastebinService = fixture.debugElement.injector.get (PastebinService); mockPaste = [id: 1, titel: "Hej värld", språk: "Ruby", klistra in: "sätter" Hello ""]; spion = spyOn (pastebinService, 'getPastebin') .and.returnValue (Promise.resolve (mockPaste));
Spionen är inte oroad över genomförandedetaljerna för den riktiga tjänsten, utan förbi ett eventuellt samtal till själva getPastebin ()
metod. Dessutom begravdes alla fjärrsamtal inuti getPastebin ()
ignoreras av våra test. Vi kommer att skriva isolerade enhetstest för Angular Services i den andra delen av handledningen.
Lägg till följande tester till pastebin.component.spec.ts
.
den ('ska inte visa pastebin före OnInit', () => this.tbody = element.querySelector ("tbody"); // Pröva detta utan att ersätta (\ s \ s + / g, ")" och se vad som händer hända (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" "," tbody should be empty "), förvänta sig (spy.calls.any ()). toBe (false, "Spy ska inte kallas ännu");); den ('ska fortfarande inte visa pastebin efter komponentinitialiserad', () => fixture.detectChanges (); // getPastebin-tjänsten är asynk, men testet är inte. förvänta (this.tbody.innerText.replace (/ \ s tobe ("", "tbody ska fortfarande vara tomt"), förvänta sig (spy.calls.any ()). toBe (true, "getPastebin ska kallas");); ("ska visa pastebin efter getPastebin löftet löser", async () => fixture.detectChanges (); fixture.whenStable (). då (() => fixture.detectChanges (); expect (comp.pastebin). toEqual (jasmine.objectContaining (mockPaste)); förvänta (element.innerText.replace (/ \ s \ s + / g, ")) toContain (mockPaste [0] .title););)
De två första proven är synkrona tester. Den första spec kontrollerar om innerText
av div
Elementet förblir tom så länge komponenten inte initialiseras. Det andra argumentet för Jasmins matchningsfunktion är valfritt och visas när testet misslyckas. Detta är till hjälp när du har flera förväntade uttalanden inom en specifikation.
I den andra specifikationen initieras komponenten (eftersom fixture.detectChanges ()
kallas), och spionen förväntas också åberopas, men mallen ska inte uppdateras. Även om spionen returnerar ett löst löfte, den mockPaste
är inte tillgänglig än. Det ska inte vara tillgängligt om inte testet är ett asynkront test.
Det tredje testet använder en async ()
funktion som diskuterats tidigare för att köra testet i en async testzon. async ()
används för att göra ett synkront test asynkron. fixture.whenStable ()
kallas när alla pågående asynkrona aktiviteter kompletteras, och sedan en andra omgång av fixture.detectChanges ()
kallas för att uppdatera DOM med de nya värdena. Förväntan i det slutliga testet säkerställer att vår DOM uppdateras med mockPaste
värden.
För att testa testen måste vi uppdatera vår pastebin.component.ts
med följande kod.
/*pastebin.component.ts*/ import Komponent, OnInit från '@ vinkel / kärna'; importera Pastebin från "... / pastebin"; importera PastebinService från "... /pastebin.service"; @Component (selector: 'app-pastebin', templateUrl: './pastebin.component.html', styleUrls: ['./pastebin.component.css']) exportklassen PastebinComponent implementerar OnInit title: string = " Pastebin Application "; pastebin: any = []; konstruktör (public pastebinServ: PastebinService) // loadPastebin () kallas init ngOnInit () this.loadPastebin (); public loadPastebin () // påbereder pastebin-tjänsten getPastebin () -metoden och lagrar svaret i "pastebin" -fastigheten this.pastebinServ.getPastebin (). då (pastebin => this.pastebin = pastebin);
Mallen behöver också uppdateras.
titel
id Titel Språk Koda Paste.id Paste.title Paste.language Visa kod
Generera en AddPaste-komponent med Angular-CLI. Bilden nedan visar konstruktionen av AddPaste-komponenten.
Komponentens logik ska passera följande specifikationer.
showModal
egendom till Sann
. (showModal
är en booleansk egenskap som blir sann när modal visas och falsk när modal är stängd.)addPaste ()
metod.showModal
egendom till falsk
.Vi har utarbetat de första tre testerna för dig. Se om du kan göra testen skickad på egen hand.
beskriv ('AddPasteComponent', () => Låt komponent: AddPasteComponent; Låt fixture: ComponentFixture; låt de: DebugElement; låt element: HTMLElement; låt spion: jasmine.Spy; låt pastebinService: PastebinService; beforeEach (async (() => TestBed.configureTestingModule (declarations: [AddPasteComponent], import: [HttpModule, FormsModule], leverantörer: [PastebinService],) .compileComponents ();)); beforeEach (() => // initialisering fixture = TestBed.createComponent (AddPasteComponent); pastebinService = fixture.debugElement.injector.get (PastebinService); komponent = fixture.componentInstance; de = fixture.debugElement.query (By.css '.add-paste')); element = de.nativeElement; spion = spyOn (pastebinService, 'addPaste') och.callThrough (); // fråga fixtur för att upptäcka ändringar fixture.detectChanges ();); det ("ska skapas", () => förvänta (komponent) .toBeTruthy ();); det ("ska visa" skapa klistra in "-knappen", () => // Det bör finnas en skapa-knapp i mallen förvänta sig (element.innerText) .toContain ("skapa klistra");); den ('ska inte visa modal om inte knappen är klickad', () => // källmodell är ett id för modal. Det ska inte dyka upp om inte knappen skapas klicka förväntade (element.innerHTML). not.toContain ("source-modal");) det ("ska visa modal när" create Paste "är klickade", () => let createPasteButton = fixture.debugElement.query (By.css ("button" )); // triggerEventHandler simulerar ett klickhändelse på knappobjektet createPasteButton.triggerEventHandler ("click", null); fixture.detectChanges (); expect (element.innerHTML) .toContain ("source-modal"); .showModal) .toBeTruthy ("showModal ska vara sant");))
DebugElement.triggerEventHandler ()
är det enda nya här. Det används för att utlösa en klickhändelse på knappelementet som det heter på. Den andra parametern är händelseobjektet, och vi har lämnat det tomt sedan komponentens klick()
Förväntar mig inte en.
Det är det för dagen. I den här första artikeln lärde vi oss:
I nästa handledning skapar vi nya komponenter, skriver fler testkomponenter med inmatningar och utdata, tjänster och rutter. Håll dig klar för den andra delen av serien. Dela dina tankar genom kommentarerna.