Test Driven Development er en programmeringspraksis som har blitt forkynt og fremmet av alle utviklingssamfunn på planeten. Og likevel er det en rutine som i stor grad overses av en utvikler samtidig som han lærer et nytt rammeverk. Skriveenhetstester fra første dag vil hjelpe deg med å skrive bedre kode, lete bugs enkelt og opprettholde en bedre utviklings arbeidsflyt.
Angular, som er en fullverdig front-end utviklingsplattform, har sitt eget sett med verktøy for testing. Vi vil bruke følgende verktøy i denne opplæringen:
det ('skal ha en definert komponent', () => forvente (komponent) .toBeDefined (););
teststed
og ComponentFixtures
og hjelperfunksjoner som async
og fakeAsync
er en del av @ Vinkel / kjerne / testing
pakke. Å bli kjent med disse verktøyene er nødvendig hvis du vil skrive tester som viser hvordan komponentene dine interagerer med sin egen mal, tjenester og andre komponenter.Vi skal ikke dekke funksjonelle tester ved hjelp av Protractor i denne opplæringen. Protractor er en populær end-to-end test ramme som samhandler med programmets brukergrensesnitt ved hjelp av en faktisk nettleser.
I denne opplæringen er vi mer opptatt av testing av komponenter og komponentens logikk. Imidlertid vil vi skrive et par tester som viser grunnleggende grensesnittinteraksjon ved hjelp av Jasmine-rammen.
Målet med denne opplæringen er å skape frontend for et Pastebin-program i et testdrevet utviklingsmiljø. I denne opplæringen følger vi den populære TDD-mantraen, som er "rød / grønn / refaktor". Vi vil skrive tester som i utgangspunktet mislykkes (rød) og deretter jobbe på vår søknadskode for å få dem til å passere (grønn). Vi skal refactor vår kode når den begynner å stinke, noe som betyr at det blir oppblåst og stygg.
Vi skal skrive tester for komponenter, deres maler, tjenester og Pastebin-klassen. Bildet nedenfor illustrerer strukturen til vår Pastebin-applikasjon. Gjenstander som er uttonet vil bli diskutert i den andre delen av opplæringen.
I første del av serien vil vi bare konsentrere oss om å sette opp testmiljøet og skrive grunnleggende tester for komponenter. Vinkel er en komponentbasert rammeverk; Derfor er det en god idé å bruke litt tid til å bli kjent med skrive tester for komponenter. I den andre delen av serien vil vi skrive mer komplekse tester for komponenter, komponenter med innganger, rutede komponenter og tjenester. Ved slutten av serien vil vi ha et fullt fungerende Pastebin-program som ser slik ut.
Utsikt over Pastebin-komponentenSe på AddPaste-komponentenI denne opplæringen lærer du hvordan du:
Hele koden for opplæringen er tilgjengelig på Github.
https://github.com/blizzerand/pastebin-angular
Klon repo og gjerne sjekke koden hvis du er i tvil på et hvilket som helst tidspunkt i denne opplæringen. La oss komme i gang!
Utviklerne på Angular har gjort det enkelt for oss å sette opp testmiljøet vårt. For å komme i gang må vi installere Angular først. Jeg foretrekker å bruke Angular-CLI. Det er en alt-i-ett-løsning som tar seg av å skape, generere, bygge og teste ditt Angular-prosjekt.
ng ny Pastebin
Her er katalogstrukturen skapt av Angular-CLI.
Siden våre interesser er mer tilbøyelige til testaspektene i Angular, må vi passe på to typer filer.
karma.conf.js er konfigurasjonsfilen for Karma-testløperen og den eneste konfigurasjonsfilen som vi trenger for å skrive enhetstester i Angular. Chrome er som standard nettleser-launcher som brukes av Karma for å fange tester. Vi vil lage en egendefinert lanseringsprogram for å kjøre den hodeløse Chrome og legge den til lesere
matrise.
/*karma.conf.js*/ browsere: ['Chrome', 'ChromeNoSandboxHeadless'], customLaunchers: ChromeNoSandboxHeadless: base: 'Chrome', flagg: ['- no-sandbox', // Se https: / /chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md '--headless', '- disable-gpu', // Uten en ekstern feilsøkingsport, avslutter Google Chrome umiddelbart. '--remote-debugging-port = 9222',],,,
Den andre typen fil som vi trenger å se etter, er alt som slutter med .spec.ts
. Ved konvensjon er testene skrevet i Jasmine kalt spesifikasjoner. Alle testspesifikasjonene skal være plassert inne i applikasjonens src / app /
katalog fordi det er der karma ser etter testspesifikasjonene. Hvis du oppretter en ny komponent eller en tjeneste, er det viktig at du plasserer testspesifikasjonene i samme katalog som koden for komponenten eller tjenesten ligger i.
De ng ny
kommandoen har opprettet en app.component.spec.ts
fil for vår app.component.ts
. Du er velkommen til å åpne den og ta en god titt på Jasmine-testene i Angular. Selv om koden ikke gir mening, er det greit. Vi vil beholde AppComponent som det er for nå, og bruk det til å være vert for rutene på et senere tidspunkt i opplæringen.
Vi trenger en Pastebin-klasse for å modellere vår Pastebin inne i komponentene og testene. Du kan opprette en ved hjelp av Angular-CLI.
ng generere klasse Pastebin
Legg til følgende logikk for Pastebin.ts:
Eksportklasse Pastebin id: nummer; tittel: streng; språk: streng; lim inn: streng; constructor (values: Object = ) Object.assign (dette, verdier); eksport const Språk = ["Ruby", "Java", "JavaScript", "C", "Cpp"];
Vi har definert en Pastebin-klasse, og hver forekomst av denne klassen har følgende egenskaper:
id
tittel
Språk
lim inn
Opprett en annen fil som heter pastebin.spec.ts
for testpakken.
/ * pastebin.spec.ts * / // importer importeringen av Pastebin-klassen Pastebin fra './pastebin'; beskrive ('Pastebin', () => it ('skal opprette en forekomst av Pastebin', () => forvente (ny Pastebin ()). toBeTruthy (););)
Testpakken starter med a beskrive
blokk, som er en global jasmin-funksjon som aksepterer to parametere. Den første parameteren er tittelen på testpakken, og den andre er den faktiske implementeringen. Spesifikasjonene er definert ved hjelp av en den
funksjon som tar to parametere, ligner den til beskrive
blokkere.
Flere spesifikasjoner (den
blokker) kan nestes inne i en test suite (beskrive
blokkere). Sørg imidlertid for at testpakettitlene er oppkalt på en slik måte at de er entydige og lesbare fordi de er ment å tjene som dokumentasjon for leseren.
Forventninger, implementert ved hjelp av forvente
funksjon, brukes av Jasmine for å avgjøre om en spesifikasjon skal passere eller mislykkes. De forvente
funksjonen tar en parameter som er kjent som den faktiske verdien. Den er deretter festet med en annen funksjon som tar forventet verdi. Disse funksjonene kalles matcherfunksjoner, og vi vil bruke matcherfunksjonene som toBeTruthy ()
, skal defineres()
, å være()
, og å inneholde()
mye i denne opplæringen.
Forvent (ny Pastebin ()). toBeTruthy ();
Så med denne koden, har vi opprettet en ny forekomst av Pastebin-klassen og forventer at den er sann. La oss legge til en annen spesifikasjon for å bekrefte at Pastebin-modellen fungerer som ønsket.
den ('skal akseptere verdier', () => la pastebin = ny Pastebin (); pastebin = id: 111, tittel: "Hei verden", språk: "Ruby" forvente (pastebin.id) .toEqual (111); expect (pastebin.language) .toEqual ("Ruby"); forvente (pastebin.paste) .toEqual ('print' Hello ''););
Vi har ordnet Pastebin-klassen og lagt til noen forventninger til vår testspesifikasjon. Løpe ng test
for å bekrefte at alle testene er grønne.
Generer en tjeneste ved hjelp av kommandoen nedenfor.
ng generere service pastebin
PastebinService
vil være vert for logikken for å sende HTTP-forespørsler til serveren; Vi har imidlertid ikke en server-API for applikasjonen vi bygger. Derfor skal vi simulere serverkommunikasjonen ved hjelp av en modul kjent som InMemoryWebApiModule.
Installere vinkel-i-minne-web-api
via npm:
npm installere vinkel-i-minne-web-api - save
Oppdater AppModule med denne versjonen.
/ * app.module.ts * / import BrowserModule fra '@ vinkel / plattform-nettleser'; importer NgModule fra '@ vinkel / kjerne'; // Komponenter importerer AppComponent fra './app.component'; // Service for Pastebin import PastebinService fra "./pastebin.service"; // Moduler som brukes i denne opplæringen importerer HttpModule fra '@ vinkel / http'; // I minnet Web api å simulere en http-server import InMemoryWebApiModule fra 'angular-in-memory-web-api'; importer InMemoryDataService fra './in-memory-data.service'; @NgModule (declarations: [AppComponent,], import: [BrowserModule, HttpModule, InMemoryWebApiModule.forRoot (InMemoryDataService),] leverandører: [PastebinService], bootstrap: [AppComponent]) eksportklasse AppModule
Lag en InMemoryDataService
som implementerer InMemoryDbService
.
/*in-memory-data.service.ts*/ import InMemoryDbService fra 'angular-in-memory-web-api'; importer Pastebin fra './pastebin'; eksport klasse InMemoryDataService implementerer InMemoryDbService createDb () const pastebin: Pastebin [] = [id: 0, tittel: "Hei verden Ruby", språk: "Ruby", lim: 'setter' Hello World '' : 1, tittel: "Hei verden C", språk: "C", lim: 'printf ("Hei verden");', id: 2, tittel: "Hei verden CPP", språk: "C ++" lim inn: 'cout<<"Hello world";', id: 3, title: "Hello world Javascript", language: "JavaScript", paste: 'console.log("Hello world")' ]; return pastebin;
Her, pastebin
er en rekke prøvepastaer som vil bli returnert eller oppdatert når vi utfører en HTTP-handling som http.get
eller http.post
.
/*pastebin.service.ts * / import Injectable fra '@ vinkel / kjerne'; importer Pastebin fra './pastebin'; importer Http, Headers fra '@ vinkel / http'; importer 'rxjs / add / operator / toPromise'; @Injectable () eksportklasse PastebinService // Prosjektet bruker InMemoryWebApi til å håndtere Server API. // Her "api / pastebin" simulerer en server API url privat pastebinUrl = "api / pastebin"; private overskrifter = nye overskrifter ('Content-Type': "application / json"); konstruktør (privat http: Http) // getPastebin () utfører http.get () og returnerer et løfte offentlig getPastebin (): Promisereturn this.http.get (this.pastebinUrl) .toPromise () .then (response => response.json (). data) .catch (this.handleError); privathåndtakError (feil: noen): Promise console.error ('En feil oppstod', feil); returnere Promise.reject (error.message || feil);
De getPastebin ()
Metoden gjør en HTTP.get-forespørsel og returnerer et løfte som løser en rekke Pastebin-gjenstander returnert av serveren.
Hvis du får en Ingen leverandør for HTTP feil mens du kjører en spesifikasjon, må du importere HTTPmodulen til den aktuelle spesifikke filen.
Komponenter er den mest grunnleggende byggeblokken til et brukergrensesnitt i en kantet applikasjon. En kantet applikasjon er et tre av kantede komponenter.
- Angular Documentation
Som fremhevet tidligere i oversikten, vil vi jobbe med to komponenter i denne opplæringen: PastebinComponent
og AddPasteComponent
. Pastebin-komponenten består av en tabellstruktur som viser all lim hentet fra serveren. AddPaste-komponenten inneholder logikken for å lage nye pastaer.
Gå videre og generer komponentene ved hjelp av Angular-CLI.
ng g komponent - spec = false Pastebin
De --spec = false
alternativet forteller Angular-CLI å ikke lage en spesifikasjonsfil. Dette skyldes at vi ønsker å skrive enhetstest for komponenter fra grunnen av. Lage en pastebin.component.spec.ts
fil inne i pastebin-komponent mappe.
Her er koden for pastebin.component.spec.ts
.
importer TestBed, ComponentFixture, async fra '@ vinkel / kjerne / testing'; importer DebugElement fra '@ vinkel / kjerne'; importer PastebinComponent fra './pastebin.component'; importer By fra '@ vinkel / plattform-nettleser'; importer Pastebin, Languages fra '... / pastebin'; // Moduler som brukes til testing av import HttpModule fra '@ vinkel / http'; Beskriv ('PastebinComponent', () => // Typescript deklarasjoner. la komp .: PastebinComponent; la fixture: ComponentFixture; la de: DebugElement; la elementet: HTMLElement; la mockPaste: Pastebin []; // førEk blir kalt en gang før hver 'det' blokk i en test. // Bruk dette til å konfigurere til komponenten, injisere tjenester osv. FørEach (() => TestBed.configureTestingModule (erklæringer: [PastebinComponent], // erklære testkomponentimporten: [HttpModule],); fixture = TestBed .createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('pastebin')); element = de.nativeElement;); )
Det skjer mye her. La oss slå opp og ta ett stykke av gangen. Innen beskrive
blokkere, vi har erklært noen variabler, og så har vi brukt en beforeEach
funksjon. beforeEach ()
er en global funksjon levert av Jasmine og, som navnet antyder, blir det påkalt en gang før hver spesifikasjon i beskrive
blokk hvor den kalles.
TestBed.configureTestingModule (declarations: [PastebinComponent], // erklære testkomponentimporten: [HttpModule],);
teststed
klassen er en del av verktøyene for vinkelprøving, og det skaper en testmodul som ligner den av @NgModule
klasse. Videre kan du konfigurere teststed
bruker configureTestingModule
metode. For eksempel kan du opprette et testmiljø for prosjektet ditt som emulerer selve Angular-applikasjonen, og du kan deretter trekke en komponent fra søknadsmodulen og legge den på nytt på denne testmodulen.
fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('div')); element = de.nativeElement;
Fra Angular dokumentasjonen:
DecreateComponent
metode returnerer aComponentFixture
, et håndtak på testmiljøet som omgir den opprettede komponenten. Fixturen gir tilgang til komponentinstansen selv og tilDebugElement
, som er et håndtak på komponentens DOM-element.
Som nevnt ovenfor har vi opprettet en fixtur av PastebinComponent
og deretter brukt den fixturen for å lage en forekomst av komponenten. Vi kan nå få tilgang til komponentens egenskaper og metoder i våre tester ved å ringe comp.property_name
. Siden fixturen gir også tilgang til debugElement
, Vi kan nå spørre om DOM-elementene og seleksjonene.
Det er et problem med vår kode som vi ennå ikke har tenkt på. Vår komponent har en ekstern mal og en CSS-fil. Å hente og lese dem fra filsystemet er en asynkron aktivitet, i motsetning til resten av koden, som er helt synkron.
Vinkel gir deg en funksjon som kalles asynkron ()
som tar seg av alle de asynkrone tingene. Hva async gjør, er å holde oversikt over alle asynkrone oppgaver i den, samtidig som du skjuler kompleksiteten til asynkron kjøring fra oss. Så vi vil nå ha to før hver funksjon, en asynkron beforeEach ()
og en synkron beforeEach ()
.
/ * pastebin.component.spec.ts * / // beforeEach kalles en gang før hver 'det' blokk i en test. // Bruk dette til å konfigurere til komponenten, injisere tjenester etc. førEach (async (() => // async før brukes til å samle eksterne maler som er en asynkaktivitet TestBed.configureTestingModule (deklarasjoner: [PastebinComponent], / / erklære testkomponentimporten: [HttpModule],) .compileComponents (); // kompilere mal og css)); beforeEach (() => // Og her er synkron async funksjon fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('pastebin')); element = de.nativeElement;);
Vi har ikke skrevet noen testspesifikasjoner ennå. Det er imidlertid en god idé å lage en oversikt over spesifikasjonene på forhånd. Bildet nedenfor viser et grovt design av Pastebin-komponenten.
Vi må skrive tester med følgende forventninger.
pastebinService
injiseres i komponenten, og metodene er tilgjengelige.onInit ()
er kalt.De tre første tester er enkle å implementere.
det ('skal ha en komponent', () => forvente (comp) .toBeTruthy ();); det ('burde ha en tittel', () => comp.title = 'Pastebin Application'; fixture.detectChanges (); expect (element.textContent) .toContain (comp.title);) et bord for å vise pastas ', () => expect (element.innerHTML) .toContain ("thead"); forvente (element.innerHTML) .toContain ("tbody");)
I et testmiljø binder Angular ikke automatisk komponentens egenskaper med malelementene. Du må eksplisitt ringe fixture.detectChanges ()
hver gang du vil binde en komponentegenskap med malen. Kjører testen bør gi deg en feil fordi vi ennå ikke har uttalt tittelegenskapen inne i komponenten vår.
tittel: string = "Pastebin Application";
Ikke glem å oppdatere malen med en grunnleggende tabellstruktur.
tittel
id Tittel Språk Kode
Når det gjelder resten av sakene, må vi injisere Pastebinservice
og skriv tester som omhandler komponent-service interaksjon. En ekte tjeneste kan ringe til en ekstern server, og det vil være en mektig og utfordrende oppgave å injisere det i råform.
I stedet bør vi skrive tester som fokuserer på om komponenten samhandler med tjenesten som forventet. Vi skal legge til spesifikasjoner som spionerer på pastebinService
og dets getPastebin ()
metode.
Først importerer du PastebinService
inn i vår test suite.
importer PastebinService fra '... /pastebin.service';
Deretter legger du til den i tilbydere
array inni TestBed.configureTestingModule ()
.
TestBed.configureTestingModule (declarations: [CreateSnippetComponent], leverandører: [PastebinService],);
Koden nedenfor lager en Jasmine spion som er designet for å spore alle samtaler til getPastebin ()
metode og returnere et løfte som umiddelbart løser til mockPaste
.
// Den virkelige PastebinService er injisert i komponenten la pastebinService = fixture.debugElement.injector.get (PastebinService); mockPaste = [id: 1, tittel: "Hei verden", språk: "Ruby", lim: "setter" Hei ""]; spion = spyOn (pastebinService, 'getPastebin') .and.returnValue (Promise.resolve (mockPaste));
Spion er ikke bekymret for implementeringsdetaljer av den virkelige tjenesten, men i stedet bypasser ethvert anrop til det faktiske getPastebin ()
metode. Dessuten begravet alle de eksterne samtalene innvendig getPastebin ()
blir ignorert av våre tester. Vi skal skrive isolerte enhetstester for Angular-tjenester i den andre delen av opplæringen.
Legg til følgende tester til pastebin.component.spec.ts
.
det ('burde ikke vise pastebin før OnInit', () => this.tbody = element.querySelector ("tbody"); // Prøv dette uten 'erstatte (\ s \ s + / g, ")' metoden og se hva som skjer forventer (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" "," tbody burde være tomt "), forventer (spy.calls.any ()). toBe (false, "Spy skal ikke bli ringt");); den ('skal fortsatt ikke vise pastebin etter komponent initialisert', () => fixture.detectChanges (); // getPastebin-tjenesten er asynk, men testen er ikke. forvent (this.tbody.innerText.replace (/ \ s tobe ("", "tbody skal fortsatt være tomt"); forvent (spy.calls.any ()). toBe (sann, 'getPastebin skal kalles');); ('skal vise pastebin etter getPastebin løftet løser', async () => fixture.detectChanges (); fixture.whenStable (). then (() => fixture.detectChanges (); expect (comp.pastebin). toEqual (jasmine.objectContaining (mockPaste)); forvente (element.innerText.replace (/ \ s \ s + / g, ")) toContain (mockPaste [0] .title););)
De to første testene er synkrontester. Den første spesifikasjonen kontrollerer om innertext
av div
elementet forblir tom så lenge komponenten ikke er initialisert. Det andre argumentet til Jasmines matcherfunksjon er valgfritt og vises når testen mislykkes. Dette er nyttig når du har flere forventninger i en spesifikasjon.
I den andre spesifikasjonen blir komponenten initialisert (fordi fixture.detectChanges ()
kalles), og spionen forventes også å bli påkalt, men malen skal ikke oppdateres. Selv om spionen returnerer et løst løfte, vil mockPaste
er ikke tilgjengelig ennå. Det bør ikke være tilgjengelig med mindre testen er en asynkron test.
Den tredje testen bruker en asynkron ()
Funksjonen diskuteres tidligere for å kjøre testen i en async test sone. asynkron ()
brukes til å gjøre en synkron test asynkron. fixture.whenStable ()
kalles når alle avventer asynkrone aktiviteter suppleres, og deretter en andre runde av fixture.detectChanges ()
kalles for å oppdatere DOM med de nye verdiene. Forventningen i den endelige testen sikrer at vår DOM er oppdatert med mockPaste
verdier.
For å få testene til å passere, må vi oppdatere vår pastebin.component.ts
med følgende kode.
/*pastebin.component.ts*/ import Component, OnInit fra '@ vinkel / kjerne'; importer Pastebin fra '... / pastebin'; importer PastebinService fra '... /pastebin.service'; @Component (selector: 'app-pastebin', templateUrl: './pastebin.component.html', styleUrls: ['./pastebin.component.css']) eksportklasse PastebinComponent implementerer OnInit title: string = " Pastebin Application "; pastebin: any = []; constructor (public pastebinServ: PastebinService) // loadPastebin () kalles på init ngOnInit () this.loadPastebin (); offentlig lastPastebin () // påkaller pastebin-tjenesten getPastebin () -metoden og lagrer svaret i "pastebin" -egenskapen this.pastebinServ.getPastebin (). da (pastebin => this.pastebin = pastebin);
Malen må også oppdateres.
tittel
id Tittel Språk Kode Paste.id Paste.title Paste.language Vis kode
Generer en AddPaste-komponent ved hjelp av Angular-CLI. Bildet nedenfor viser utformingen av AddPaste-komponenten.
Komponentets logikk skal passere følgende spesifikasjoner.
showModal
eiendom til ekte
. (showModal
er en boolsk egenskap som blir sant når modal vises og falsk når modal er stengt.)addPaste ()
metode.showModal
eiendom til falsk
.Vi har utarbeidet de første tre tester for deg. Se om du kan få testene til å passere på egen hånd.
beskriv ('AddPasteComponent', () => la komponent: AddPasteComponent; la fixture: ComponentFixture; la de: DebugElement; la elementet: HTMLElement; la spion: jasmine.Spy; la pastebinService: PastebinService; beforeEach (async (() => TestBed.configureTestingModule (declarations: [AddPasteComponent], import: [HttpModule, FormsModule], leverandø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'). og.callThrough (); // spørre fixtur for å oppdage endringer fixture.detectChanges ();); det ('skal opprettes', () => forvente (komponent) .toBeTruthy ();); det ('skal vise' opprett lim '-knappen', () => // Det bør opprettes en opprettingsknapp i malen (element.innerText) .toContain ("create Paste");); det skal ikke vise modal med mindre knappen er klikket, () => // kildemodell er et id for modal. Det skal ikke vises hvis ikke skaperknappen klikkes forventer (element.innerHTML). not.toContain ("source-modal");) det ("skal vise modal når" create Paste "er klikket", () => la createPasteButton = fixture.debugElement.query (By.css ("button" )); // triggerEventHandler simulerer et klikkhendelse på knappobjektet createPasteButton.triggerEventHandler ("klikk", null); fixture.detectChanges (); expect (element.innerHTML) .toContain ("source-modal"); .showModal) .toBeTruthy ("showModal should be true");)
DebugElement.triggerEventHandler ()
er det eneste nye her. Det brukes til å utløse et klikkhendelse på knappelementet som det kalles på. Den andre parameteren er hendelsesobjektet, og vi har forlatt det tomt siden komponenten er Klikk ()
Forventer ikke en.
Det er det for dagen. I denne første artikkelen lærte vi:
I den neste opplæringen lager vi nye komponenter, skriver flere testkomponenter med innganger og utdata, tjenester og ruter. Hold deg innstilt for den andre delen av serien. Del dine tanker gjennom kommentarene.