Testing din Ruby Code med Guard, RSpec & Pry

Mitt siste arbeid har vært på et skybasert Ruby-prosjekt for BBC News, kommende 2014-valg. Det krever rask I / O, skalerbarhet og må testes godt. Kravet "være godt testet" er det jeg vil fokusere på i denne opplæringen.

Introduksjon

Dette prosjektet bruker noen forskjellige Amazon-tjenester som:

  • SQS (Simple Queue Service)
  • DynamoDB (nøkkel / verdi butikk)
  • S3 (Enkel lagringstjeneste)

Vi må kunne skrive tester som er raske og gi oss øyeblikkelig tilbakemelding om problemer med vår kode.

Selv om vi ikke skal bruke Amazon-tjenester i denne opplæringen, nevner jeg dem, fordi vi for å få tester som er raske, krever at vi forfalsker disse eksterne objektene (for eksempel, vi trenger ikke en nettverkstilkobling for å kjøre vår tester, fordi den avhengigheten kan resultere i sakte kjørende tester).

Ved siden av den tekniske lederen Robert Kenny (som er veldig godt kjent med å skrive TDD (testdrevet utvikling) basert Ruby-applikasjoner) har vi benyttet ulike verktøy som har gjort denne prosessen og programmeringen vår fungerer mye lettere.

Jeg vil dele litt informasjon om disse verktøyene med deg.

Verktøyene jeg skal dekke er:

  • RSpec (testing rammeverk)
  • Guard (oppgave løper)
  • Pry (REPL og feilsøking)

Hva trenger jeg å vite på forhånd?

Jeg skal anta at du er kjent med Ruby-koden og Ruby Eco-systemet. For eksempel, jeg trenger ikke å forklare for deg hva "edelstener" er eller hvordan visse Ruby-syntaks / konsepter fungerer.

Hvis du er usikker, så før du går, vil jeg anbefale å lese et av mine andre innlegg på Ruby for å komme deg opp til fart.

Vakt

Du er kanskje ikke kjent med Guard, men i hovedsak er det et kommandolinjeverktøy som bruker Ruby til å håndtere ulike hendelser.

For eksempel kan Guard varsle deg når bestemte filer har blitt redigert, og du kan utføre en handling basert på typen fil eller hendelse som ble avfyrt.

Dette er kjent som en "task runner", du har kanskje hørt uttrykket før, da de får mye bruk i front-end / client-side verden for øyeblikket (Grunt and Gulp er to populære eksempler).

Grunnen til at vi bruker Guard er fordi det bidrar til å gjøre tilbakemeldingssløyfen (når du gjør TDD) mye strammere. Det tillater oss å redigere testfiler, se en feiltest, oppdater og lagre koden vår og umiddelbart se om den går eller mislykkes (avhengig av hva vi skrev).

Du kan bruke noe som Grunt eller Gulp i stedet, men vi foretrekker å bruke disse typer oppgaveløpere for å håndtere front-end / client-side ting. For back-end / server-side kode bruker vi Rake and Guard.

RSpec

RSpec, hvis du ikke var klar over det, er et testverktøy for Ruby programmeringsspråk.

Du kjører testene dine (bruker RSpec) via kommandolinjen, og jeg vil demonstrere hvordan du kan gjøre denne prosessen enklere ved bruk av Ruby's build program, Rake.

Lirk

Til slutt bruker vi en annen Ruby-perle som heter Pry, som er et ekstremt kraftig Ruby-feilsøkingsverktøy som injiserer seg selv i søknaden din, mens den kjører, slik at du kan inspisere koden din og finne ut hvorfor noe ikke fungerer.

TDD (testdrevet utvikling)

Selv om det ikke er nødvendig for å demonstrere bruken av RSpec og Guard, er det verdt å merke seg at jeg fullt ut støtter bruken av TDD som et middel for å sikre at hver linje av kode du skriver har en hensikt og er utformet på en testbar og pålitelig måte.

Jeg skal detaljere hvordan vi skulle gjøre TDD med en enkel applikasjon, så i det minste får du en følelse av hvordan prosessen fungerer.

Opprette et eksempel prosjekt

Jeg har laget et grunnleggende eksempel på GitHub for å spare deg for å skrive alt ut selv. Du er velkommen til å laste ned koden. 

La oss nå gå videre og gjennomgå dette prosjektet, steg for steg.

Primærfiler

Det er tre primære filer som kreves for at vår eksempelapplikasjon skal fungere, disse er:

  1. Gemfile
  2. Guardfile
  3. Rakefile

Vi går snart over innholdet til hver fil, men det første vi trenger å gjøre er å få vår katalogstruktur på plass.

Katalogstruktur

For vårt eksempelprosjekt trenger vi to mapper opprettet:

  • lib (dette vil holde vår søknadskode)
  • spec (dette vil holde testkoden vår)

Dette er ikke et krav for din søknad, du kan enkelt justere koden i våre andre filer for å jobbe med hvilken struktur som passer deg.

Installasjon

Åpne opp terminalen din og kjør følgende kommando:

perle installasjonspakker

Bundler er et verktøy som gjør det enklere å installere andre perler.

Når du har kjørt den kommandoen, lager du de tre filene ovenfor (Gemfile, Guardfile og Rakefile).

Gemfile

De Gemfile er ansvarlig for å definere en liste over avhengigheter for vår søknad.

Slik ser det ut:

kilde "https://rubygems.org" gem 'rspec' gruppe: utvikling gjør gem 'guard' perle 'guard-rspec' perle 'pry' end 

Når denne filen er lagret, kjør du kommandoen bunt installasjon.

Dette vil installere alle våre edelstener for oss (inkludert de edelstenene som er angitt i utvikling gruppe).

Formålet med utvikling gruppe (som er en bunter-spesifikk funksjon), er det da du distribuerer applikasjonen din, kan du fortelle produksjonsmiljøet ditt for å installere bare de perlene som kreves for at søknaden skal fungere skikkelig.

Så for eksempel, alle edelstener inne i utvikling gruppen, er ikke nødvendig for at programmet skal fungere skikkelig. De brukes bare til å hjelpe oss mens vi utvikler og tester koden vår.

For å installere de riktige edelstenene på produksjonsserveren din, må du kjøre noe som:

buntinstallasjon - uten utvikling

Rakefile

De Rakefile vil tillate oss å kjøre RSpec tester fra kommandolinjen.

Slik ser det ut:

krever 'rspec / core / rake_task' RSpec :: Kjerne :: RakeTask.new do | oppgave | task.rspec_opts = ['--color', '--format', 'doc'] slutten 

Merk: Du trenger ikke Guard for å kunne kjøre RSpec-testene dine. Vi bruker Guard for å gjøre det lettere å gjøre TDD.

Når du installerer RSpec, gir den deg tilgang til en innebygd Rake-oppgave, og det er det vi bruker her.

Vi lager en ny forekomst av RakeTask som som standard oppretter en oppgave som kalles spec som vil se etter en mappe som heter spec og vil kjøre alle testfilene i den mappen, ved hjelp av konfigurasjonsalternativene vi har definert.

I dette tilfellet vil vi at shell-utdataene skal ha farge og vi vil formatere utdataene til doc stil (du kan endre formatet til å være nestet som et eksempel).

Du kan konfigurere Rake-oppgaven til å fungere som du vil, og å se etter forskjellige kataloger, hvis det er det du har. Men standardinnstillingene fungerer bra for applikasjonen vår, så det er det vi skal bruke.

Nå, hvis jeg vil kjøre testene i mitt eksempel GitHub-depot, må jeg åpne min terminal og kjøre kommandoen:

rake spec

Dette gir oss følgende utgang:

rake spec / bin / ruby ​​-S rspec ./spec/example_spec.rb --farger --format doc RSpecGreeter RSpecGreeter # greet () Ferdig på 0.0006 sekunder 1 eksempel, 0 feil 

Som du kan se er det null feil. Det er fordi selv om vi ikke har noen søknadskode skrevet, har vi heller ikke noen testkode skrevet heller.

Guardfile

Innholdet i denne filen forteller Guard hva du skal gjøre når vi kjører vakt kommando:

vakt 'rspec' gjør # watch / lib / files watch (% r ^ lib /(.+) .rb $) gjør | m | "spec / # m [1] _ spec.rb" end # watch / spec / files watch (% r ^ spec /(.+) .rb $) gjør | m | "spec / # m [1]. rb" slutten 

Du har lagt merke til i vår Gemfile vi angav perlen: vakt-rspec. Vi trenger den perlen for å tillate Guard å forstå hvordan vi håndterer endringer i RSpec-relaterte filer.

Hvis vi ser igjen på innholdet, kan vi se det hvis vi kjørte vakt rspec da Guard ville se de angitte filene og utføre de angitte kommandoene når noen endringer i disse filene hadde skjedd.

Merk: fordi vi bare har en vaktoppgave, rspec, så kjøres det som standard hvis vi kjørte kommandoen vakt.

Du kan se Guard gir oss en se funksjon som vi passerer en vanlig uttrykk for å gi oss mulighet til å definere hvilke filer vi er interessert i å se på.

I dette tilfellet forteller vi Guard for å se alle filene i vår lib og spec mapper og hvis noen endringer forekommer i noen av disse filene, så å utføre testfilene i vår spec mappe for å sikre at ingen endringer vi gjorde brøt våre tester (og deretter brøt ikke vår kode).

Hvis du har alle filene lastet ned fra GitHub repo, kan du prøve kommandoen selv.

Løpe vakt og lagre deretter en av filene for å se at det kjører testene.

Testkode

Før vi begynner å se på noen test- og applikasjonskode, la meg forklare hva vår søknad skal gjøre. Vår søknad er en enkelt klasse som vil returnere en hilsen melding til den som kjører koden.

Våre krav er med rette forenklet, da det vil gjøre prosessen vi skal gjøre lettere å forstå.

La oss nå se på en eksempelspesifikasjon (for eksempel vår testfil) som beskriver våre krav. Etter det begynner vi å gå gjennom koden som er definert i beskrivelsen og se hvordan vi kan bruke TDD for å hjelpe oss med å skrive søknaden vår.

Vår første test

Vi skal lage en fil med tittelen example_spec.rb. Formålet med denne filen er å bli vår spesifikasjonsfil (med andre ord, dette kommer til å bli vår testkode og representerer forventet funksjonalitet).

Grunnen til at vi skriver vår testkode før du skriver vår faktiske applikasjonskode er fordi det til slutt betyr at noen programkode vi produserer vil eksistere fordi den faktisk ble brukt.

Det er et viktig punkt jeg lager, og så la meg ta et øyeblikk for å klargjøre det mer detaljert.

Skrive testkode før programkode

Vanligvis, hvis du skriver din søknadskode først (slik at du ikke gjør TDD), så vil du finne deg selv skrive kode som på et tidspunkt i fremtiden er overutviklet og potensielt foreldet. Gjennom prosessen med refactoring eller endring av krav, kan det hende du oppdager at noen funksjoner aldri vil bli kalt.

Derfor er TDD betraktet som den bedre praksis og den foretrukne utviklingsmetoden å bruke, fordi hver linje av kode du produserer vil bli produsert av en grunn: å få en sviktende spesifikasjon (din faktiske bedriftskrav) å passere. Det er en veldig kraftig ting å huske på.

Her er vår testkode:

krever 'spec_helper' beskrive 'RSpecGreeter' gjør det 'RSpecGreeter # greet ()' greeter = RSpecGreeter.new # Gratulerer med greeting = greeter.greet # Når hilsen.should eq ('Hello RSpec!') 

Du kan legge merke til kodens kommentarer på slutten av hver linje:

  • gitt
  • Når
  • Deretter

Disse er en form for BDD (Behavior-Driven Development) terminologi. Jeg inkluderte dem for lesere som er mer kjent med BDD (Behavior-Driven Development) og som var interessert i hvordan de kan likestille disse uttalelsene med TDD.

Det første vi gjør i denne filen er belastning spec_helper.rb (som finnes i samme katalog som vår spesifikke fil). Vi kommer tilbake og ser på innholdet i den filen på et øyeblikk.

Deretter har vi to kodeblokker som er spesifikke for RSpec:

  • Beskriv 'x' gjør
  • det gjør det

Den første beskrive blokk skal tilstrekkelig beskrive den spesifikke klassen / modulen vi jobber med og gir tester for. Du kan veldig godt ha flere beskrive blokkerer i en enkelt spesifikasjonsfil.

Det er mange forskjellige teorier om hvordan du bruker beskrive og den beskrivelse blokker. Jeg personlig foretrekker enkelhet, og jeg bruker identifikasjonene for klassen / modulene / metodene som vi skal teste. Men du finner ofte noen mennesker som foretrekker å bruke fulle setninger for sine beskrivelser. Verken er rett eller galt, bare personlig preferanse.

De den blokk er forskjellig og bør alltid plasseres inne i a beskrive blokkere. Det bør forklare hvordan Vi vil at vår søknad skal fungere.

Igjen kan du bruke en vanlig setning for å beskrive kravene, men jeg har funnet ut at det i noen tilfeller kan føre til at beskrivelsene blir for eksplisitte, da de virkelig burde være mer implisitt. Å være mindre eksplisitt reduserer sjansene for endringer i funksjonaliteten din, noe som gjør at beskrivelsen din blir utdatert (må oppdatere beskrivelsen hver gang mindre funksjonalitetsendringer oppstår, er mer av en byrde enn en hjelp). Ved å bruke identifiseringen av metoden vi tester (for eksempel navnet på metoden vi kjører) kan vi unngå dette problemet.

Innholdet i den blokk er koden vi skal teste.

I eksempelet ovenfor lager vi en ny forekomst av klassen RSpecGreeter (som ikke eksisterer ennå). Vi sender meldingen hilse på (som også ikke eksisterer ennå) til det opprettede objektet som er opprettet (Merk: disse to linjene er standard ruby ​​kode på dette punktet).

Til slutt forteller vi testrammen at vi forventer utfallet av å ringe hilse på metode for å være teksten "Hei RSpec!", ved å bruke RSpec-syntaksen: eq (noe).

Legg merke til hvordan syntaksen gjør det lett å lese (selv av en ikke-teknisk person). Disse er kjent som påstander.

Det er mange forskjellige RSpec påstander, og vi vil ikke gå inn i detaljene, men gjerne gå gjennom dokumentasjonen for å se alle funksjonene RSpec gir.

Opprette en hjelper til vår test

Det er en viss mengde kjele som kreves for at våre tester skal kunne kjøre. I dette prosjektet har vi bare en spesifikasjonsfil, men i et reelt prosjekt vil du sannsynligvis ha dusinvis (avhengig av størrelsen på søknaden din).

For å hjelpe oss med å redusere boilerplate-koden, plasserer vi den inne i en spesiell hjelpefil som vi laster fra våre spesifikasjonsfiler. Denne filen vil bli navngitt spec_helper.rb.

Denne filen vil gjøre et par ting:

  • Fortell Ruby hvor vår viktigste applikasjonskode er plassert
  • last inn vår søknadskode (for at testene skal løpe)
  • last inn lirke perle (hjelper oss med å feilsøke vår kode, hvis vi trenger).

Her er koden:

$ << File.join(File.dirname(FILE), '… ', 'lib') require 'pry' require 'example' 

Merk: Første linjen kan se litt kryptisk ut, så la meg forklare hvordan det virker. Her sier vi at vi vil legge til / Lib / mappe til Ruby's $ LOAD_PATH systemvariabel. Når Ruby vurderer krever 'some_file' den har en liste over kataloger den vil prøve og finne den filen. I dette tilfellet sørger vi for at hvis vi har koden kreve "eksempel" at Ruby vil kunne finne det fordi det vil sjekke vårt / Lib / katalog og der, finner den filen som er angitt. Dette er et smart trick du vil se brukt i mange Ruby edelstener, men det kan være ganske forvirrende hvis du aldri har sett det før.

Søknadskode

Vår søknadskode kommer til å være inne i en fil med tittelen example.rb.

Før vi begynner å skrive noen søknadskode, husk at vi gjør dette prosjektet TDD. Så vi skal la testene i vår spesifikasjonsfil veilede oss om hva de skal gjøre først.

La oss begynne med å anta at du bruker vakt å kjøre testene dine (så hver gang vi gjør en endring til example.rb, Vakt vil legge merke til endringen og fortsette å løpe example_spec.rb for å sikre at våre tester passerer).

For oss å gjøre TDD riktig, vår example.rb filen vil være tom, og så hvis vi åpner filen og "lagrer" den i sin nåværende tilstand, så vil Guard løpe, og vi vil oppdage (ikke overraskende) at vår test vil mislykkes:

Feil: 1) RSpecGreeter RSpecGreeter # greet () Feil / Feil: greeter = RSpecGreeter.new # Gitt navnError: uninitialized constant RSpecGreeter # ./spec/example_spec.rb:5:inblokk (2 nivåer) i 'Ferdig i 0,00059 sekunder 1 eksempel, 1 feil Feilte eksempler: rspec ./spec/example_spec.rb:4 # RSpecGreeter RSpecGreeter # greet ()

Nå før vi går videre, la meg klargjøre at TDD er basert på premissene at hver linje med kode har grunn til å eksistere, så ikke begynn å løpe fremover og skrive ut mer kode enn du trenger. Skriv bare den minste koden som kreves for at testen skal passere. Selv om koden er stygg eller ikke oppfyller full funksjonalitet.

Poenget med TDD er å ha en tett feedback loop, også kjent som 'rød, grønn, refactor'). Hva dette betyr i praksis er:

  • skriv en feiltest
  • skriv minst koden for å få det til å passere
  • refactor koden

Du ser et øyeblikk at fordi våre krav er så enkle, er det ikke nødvendig for oss å refactor. Men i et reelt prosjekt med mye mer komplekse krav, vil du sannsynligvis trenge det tredje trinnet og refactor koden du skrev inn for å få testen til å passere.


Kommer tilbake til vår sviktende test, som du kan se i feilen, er det nei RSpecGreeter klasse definert. La oss fikse det og legge til følgende kode og lagre filen slik at testene våre kjører:

klasse RSpecGreeter # kode vil til slutt gå her slutten 

Dette vil resultere i følgende feil:

Feil: 1) RSpecGreeter RSpecGreeter # greet () Feil / Feil: greeter = greeter.greet # Når NoMethodError: undefined methodgreet 'for # # ./spec/example_spec.rb:6:in' blokk (2 nivåer) i 'Ferdig i 0,00036 sekunder 1 eksempel, 1 feil 

Nå kan vi se at denne feilen forteller oss metoden hilse på eksisterer ikke, så la oss legge til det og lagre igjen filen vår for å kjøre testene våre:

klasse RSpecGreeter def greet # kode vil til slutt gå her slutten 

OK, vi er nesten der. Feilen vi får nå er:

Feil: 1) RSpecGreeter RSpecGreeter # greet () Feil / Feil: greeter = greeting.should eq ('Hello RSpec!') # Så forventet: "Hei RSpec!" fikk: null (sammenlignet med ==) # ./spec/example_spec.rb:7:in 'blokk (2 nivåer) i' Ferdig på 0,00067 sekunder 1 eksempel, 1 feil 

RSpec forteller oss at det var ventet å se Hei RSpec! men i stedet fikk den nil (fordi vi definerte hilse på metode, men faktisk ikke definere noe inne i metoden, og så kommer det tilbake nil).

Vi legger til resten av koden som vil få testen vår til å passere:

klasse RSpecGreeter def greet "Hei RSpec!" slutten 

Der har vi det, en bestått test:

Ferdig på 0.00061 sekunder 1 eksempel, 0 feil 

Vi er ferdige her. Vår test er skrevet og koden går forbi.

Konklusjon

Så langt har vi brukt en testdrevet utviklingsprosess for å bygge applikasjonen vår, sammen med å bruke det populære RSpec testrammen.

Vi kommer til å forlate det her for nå. Kom tilbake og bli med oss ​​for del to hvor vi skal se på flere RSpec-spesifikke funksjoner, samt å bruke Pry-perlen for å hjelpe deg med å feilsøke og skrive koden din.