Før eller senere, er alle utviklere pålagt å samhandle med en API. Den vanskeligste delen er alltid relatert til pålitelig testing av koden vi skriver, og som vi vil sørge for at alt fungerer som det skal, kjører vi kontinuerlig kode som spørrer API-en selv. Denne prosessen er sakte og ineffektiv, da vi kan oppleve nettverksproblemer og datainnstøtelser (API-resultatene kan endres). La oss se på hvordan vi kan unngå all denne innsatsen med Ruby.
"Flow er viktig: skriv testene, kjør dem og se dem mislykkes, skriv så minimal implementeringskode for å få dem til å passere. Når de alle gjør det, refactor om nødvendig."
Målet vårt er enkelt: skriv et lite omslag rundt Dribbble API for å hente informasjon om en bruker (kalt 'spiller' i Dribbble-verdenen).
Som vi skal bruke Ruby, vil vi også følge en TDD-tilnærming: Hvis du ikke er kjent med denne teknikken, har Nettuts + en god primer på RSpec du kan lese. I et nøtteskall, vil vi skrive tester før du skriver vår kodeimplementering, noe som gjør det lettere å se på feil og for å oppnå en høy kodekvalitet. Flow er viktig: skriv testene, kjør dem og se dem mislykkes, skriv deretter den minste implementeringskoden for å få dem til å passere. Når de alle gjør, refactor om nødvendig.
Dribbble API er ganske grei. På dette tidspunktet støtter det bare GET-forespørsler og krever ikke autentisering: en ideell kandidat for vår opplæring. Videre tilbyr den en 60 grensesnitt per minutt, en begrensning som perfekt viser hvorfor det er en smart tilnærming å jobbe med APIer.
Denne opplæringen må anta at du har noen kjennskap til testkonsepter: inventar, mocks, forventninger. Testing er et viktig tema (spesielt i Ruby-samfunnet), og selv om du ikke er en rubyist, vil jeg oppfordre deg til å grave dypere inn i saken og søke etter tilsvarende verktøy for ditt daglige språk. Du vil kanskje lese "The RSpec-boken" av David Chelimsky et al., En utmerket primer på Behavior Driven Development.
For å oppsummere her, her er tre sentrale begreper du må vite:
"Som en vanlig praksis, kjør test hver gang du oppdaterer dem."
WebMock er et Ruby mocking bibliotek som brukes til å mock (eller stub) http forespørsler. Med andre ord kan du simulere noen HTTP-forespørsel uten å faktisk lage en. Den primære fordelen ved dette er å kunne utvikle og teste mot hvilken som helst HTTP-tjeneste uten å måtte ha selve tjenesten og uten å pådra seg relaterte problemer (som API-grenser, IP-restriksjoner og lignende).
VCR er et komplementært verktøy som registrerer noen ekte http-forespørsel og lager en fixtur, en fil som inneholder alle nødvendige data for å replikere den forespørselen uten å utføre den igjen. Vi vil konfigurere den til å bruke WebMock til å gjøre det. Med andre ord, våre tester vil samhandle med den virkelige Dribbble API bare én gang: etter det vil WebMock stubbe alle forespørsler takket være dataene som er innspilt av VCR. Vi vil få en perfekt kopi av Dribbble API-responsene som er registrert lokalt. I tillegg lar WebMock oss enkelt og konsekvent teste kantkasser (som forespørselen utløper). En fantastisk konsekvens av oppsettet vårt er at alt blir ekstremt raskt.
Når det gjelder enhetstesting, bruker vi Minitest. Det er et raskt og enkelt enhetstestbibliotek som også støtter forventninger i RSpec-mote. Det gir et mindre funksjonssett, men jeg finner at dette faktisk oppfordrer og presser deg til å skille logikken din i små, testbare metoder. Minitest er en del av Ruby 1.9, så hvis du bruker det (jeg håper det) trenger du ikke å installere det. På Ruby 1.8 er det bare et spørsmål om perle installasjon minitest
.
Jeg skal bruke Ruby 1.9.3: Hvis du ikke gjør det, vil du trolig støte på noen problemer relatert til require_relative
, men jeg har tatt med tilbakekallingskoden i en kommentar rett under den. Som en generell praksis bør du kjøre tester hver gang du oppdaterer dem, selv om jeg ikke vil nevne dette trinnet eksplisitt gjennom hele opplæringen.
Vi vil bruke den konvensjonelle / lib
og / spec
mappestruktur for å organisere vår kode. Når det gjelder navnet på biblioteket, kaller vi det Dish, Følg Dribbble-konvensjonen om å bruke basketballrelaterte termer.
Gemfile vil inneholde alle våre avhengigheter, om enn de er ganske små.
kilde: rubygems perle 'httparty' gruppe: test gjør perle 'webmock' perle 'vcr' perle 'slå' perle 'rake' ende
Httparty er en enkel å bruke perle for å håndtere HTTP-forespørsler; det vil være kjernen i vårt bibliotek. I testgruppen vil vi også legge til Turn for å endre utgangen av testene våre for å være mer beskrivende og for å støtte farge.
De / lib
og / spec
mapper har en symmetrisk struktur: for hver fil som finnes i / Lib / parabol
mappe, det burde være en fil inni / Spec / parabol
med samme navn og '_spec' suffiks.
La oss begynne med å lage en /lib/dish.rb
fil og legg til følgende kode:
krever "httparty" Dir [Fil.dirname (__ FILE__) + '/dish/*.rb'].each do | file | krever fil slutten
Det gjør ikke mye: det krever 'httparty' og deretter iterates over hver .rb
fil innsiden / Lib / parabol
å kreve det. Med denne filen på plass, vil vi kunne legge til noen funksjoner i separate filer i / Lib / parabol
og få den automatisk lastet bare ved å kreve denne enkeltfilen.
La oss flytte til / spec
mappe. Her er innholdet i spec_helper.rb
fil.
# Vi trenger den faktiske biblioteksfilen require_relative '... / lib / dish' # For Ruby < 1.9.3, use this instead of require_relative # require(File.expand_path('… /… /lib/dish', __FILE__)) #dependencies require 'minitest/autorun' require 'webmock/minitest' require 'vcr' require 'turn' Turn.config do |c| # :outline - turn's original case/test outline mode [default] c.format = :outline # turn on invoke/execute tracing, enable full backtrace c.trace = true # use humanized test names (works only with :outline format) c.natural = true end #VCR config VCR.config do |c| c.cassette_library_dir = 'spec/fixtures/dish_cassettes' c.stub_with :webmock end
Det er ganske mange ting her verdt å merke seg, så la oss slå det stykke for stykke:
require_relative
erklæring er en Ruby 1.9.3 tillegg. minitest / autorun
inkluderer alle forventningene vi skal bruke, webmock / minitest
legger til de nødvendige bindingene mellom de to bibliotekene, mens vcr
og sving
er ganske selvforklarende. Sist, men ikke minst, Rakefile
som inneholder noen støttekode:
krever 'rake / testtask' Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = sann ende oppgave: standard =>: test
De rake / testtask
biblioteket inneholder en TestTask
klasse som er nyttig for å angi plasseringen av testfiler. Fra nå av, for å kjøre våre spesifikasjoner, skriver vi bare rake
fra bibliotekets rotkatalog.
Som en måte å teste konfigurasjonen på, la vi legge til følgende kode til /lib/dish/player.rb
:
modul Oppvaskklasse Spillerendens ende
Deretter /spec/lib/dish/player_spec.rb
:
require_relative '... / ... / spec_helper' # For Ruby < 1.9.3, use this instead of require_relative # require (File.expand_path('./… /… /… /spec_helper', __FILE__)) describe Dish::Player do it "must work" do "Yay!".must_be_instance_of String end end
Løping rake
bør gi deg en test som går forbi og ingen feil. Denne testen er på ingen måte nyttig for vårt prosjekt, men det implisitt verifiserer at bibliotekets filstruktur er på plass (den beskrive
blokk ville kaste en feil hvis Oppvask :: Player
modulen ble ikke lastet inn).
For å fungere skikkelig krever Dish Httparty-modulene og det riktige base_uri
, dvs. basisadressen til Dribbble API. La oss skrive de relevante testene for disse kravene i player_spec.rb
:
... beskjed Skål :: Spiller gjør beskrive "standardattributter" gjør det "må inkludere httparty metoder" gjør oppvask :: Player.must_include HTTParty avslutte det "må ha basisadressen sett til Dribble API-endepunktet" gjør oppvask :: Player.base_uri .must_equal 'http://api.dribbble.com' slutten slutten
Som du kan se, Minitest forventninger er selvforklarende, spesielt hvis du er en RSpec-bruker: Den største forskjellen er ordlyd, hvor Minitest foretrekker "må / ikke" til "should / should_not".
Hvis du kjører disse testene, vises en feil og en feil. For å få dem til å passere, la vi legge til våre første linjer med implementeringskoden til player.rb
:
Modul Dish Class Player inkluderer HTTParty base_uri 'http://api.dribbble.com' slutten
Løping rake
igjen skulle vise de to spesifikasjonene som passerer. Nå vår Spiller
Klassen har tilgang til alle Httparty klassemetoder, som få
eller post
.
Som vi skal jobbe på Spiller
klasse, må vi ha API-data for en spiller. Dokumentasjonssiden for Dribbble API viser at sluttpunktet for å få data om en bestemt spiller er http://api.dribbble.com/players/:id
Som i typisk Rails mote, : id
er enten id eller brukernavn av en bestemt spiller. Vi skal bruke simplebits
, Brukernavnet til Dan Cederholm, en av Dribbble-grunnleggerne.
For å registrere forespørselen med video, la oss oppdatere vår player_spec.rb
fil ved å legge til følgende beskrive
blokkere til spesifikasjonen, like etter den første:
... beskrive "GET-profil" gjør før VCR.insert_cassette 'player',: record =>: new_episodes slutte etter at VCR.eject_cassette avslutte det "registrerer fixturen" gjør Dish :: Player.get ('/ players / simplebits') sluttendens ende
Etter å ha kjørt
rake
, Du kan bekrefte at armaturet er opprettet. Fra nå av vil alle våre tester være helt nettverksuavhengige.
De før
Blokk brukes til å utføre en bestemt del av kode før enhver forventning: Vi bruker den til å legge til videobåndmakroen som brukes til å registrere en fixtur som vi vil kalle "spiller". Dette vil skape en player.yml
filen under spec / inventar / dish_cassettes
. De :ta opp
alternativet er satt til å registrere alle nye forespørsler en gang og spille på dem på hver etterfølgende, identisk forespørsel. Som et bevis på konseptet kan vi legge til en spesifikasjon hvis eneste mål er å registrere en fixtur for simplebits profil. De etter
Direktivet forteller at videospilleren fjerner kassetten etter testene, og sørger for at alt er ordentlig isolert. De få
metode på Spiller
Klassen er tilgjengelig, takket være inkluderingen av Httparty
modul.
Etter å ha kjørt rake
, Du kan bekrefte at armaturet er opprettet. Fra nå av vil alle våre tester være helt nettverksuavhengige.
Hver Dribbble-bruker har en profil som inneholder en ganske omfattende mengde data. La oss tenke på hvordan vi vil at biblioteket vårt skal være da det faktisk brukes: Dette er en nyttig måte å kutte ut på. DSL vil fungere. Her er det vi vil oppnå:
simplebits = Skål :: Player.new ('simplebits') simplebits.profile => #retrerer en hash med alle dataene fra APIen simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157
Enkelt og effektivt: Vi ønsker å instantiere en spiller ved å bruke brukernavnet og deretter få tilgang til dataene sine ved å ringe metoder på forekomsten som kartlegger attributter returnert av API. Vi må være konsistente med API-en selv.
La oss takle en ting om gangen og skrive noen tester relatert til å få spillerdataene fra APIen. Vi kan endre vår "GET profil"
blokkere for å ha:
beskrive "GET-profil" la (: spiller) Skål :: Spiller.nov før gjør VCR.insert_cassette 'player',: record =>: new_episodes slutte etter at VCR.eject_cassette avslutte det "må ha en profilmetode" player.must_respond_to: profil avslutte det "må analysere api responsen fra JSON til Hash" gjør player.profile.must_be_instance_of Hash avslutte det "må utføre forespørselen og få dataene" gjør player.profile ["brukernavn"]. måleverdige 'simplebits 'slutten
De la
Direktivet øverst skaper en Oppvask :: Player
eksempel tilgjengelig i forventningene. Deretter ønsker vi å sikre at spilleren vår har en profilmetode hvis verdi er en hash som representerer dataene fra API. Som et siste skritt tester vi en prøvenøkkel (brukernavnet) for å sikre at vi faktisk utfører forespørselen.
Vær oppmerksom på at vi ennå ikke håndterer hvordan du angir brukernavnet, da dette er et ytterligere trinn. Den minimale gjennomføringen som kreves er følgende:
... Class Player inkluderer HTTParty base_uri 'http://api.dribbble.com' def profil self.class.get '/ players / simplebits' slutten ...
En veldig liten mengde kode: Vi pakker bare et få anrop i profil
metode. Vi sender så den hardkodede banen for å hente simplebits data, data som vi allerede hadde lagret takket være VCR.
Alle våre tester skal passere.
Nå som vi har en fungerende profilfunksjon, kan vi ta vare på brukernavnet. Her er de relevante spesifikasjonene:
Beskriv "standard forekomstattributter" gjør la (: spiller) Skål :: Player.new ('simplebits') det "må ha et id attributt" do player.must_respond_to: brukernavn slutt det "må ha den riktige id" .username.must_equal 'simplebits' end-end beskriver "GET-profil" gjør la (: spiller) Dish :: Player.new ('simplebits') før VCR.insert_cassette 'base',: record =>: new_episodes slutter etter gjør VCR.eject_cassette det "må ha en profilmetode" gjør player.must_respond_to: profil avslutte det "må analysere api responsen fra JSON til Hash" gjør player.profile.must_be_instance_of Hash avslutte det "må få den riktige profilen" gjør spilleren .profile ["brukernavn"]. must_equal "simplebits" slutten
Vi har lagt til en ny beskrivningsblokk for å sjekke brukernavnet vi skal legge til og bare endret spiller
initialisering i GET profil
blokkere for å reflektere DSL vi vil ha. Kjører spesifikasjonene nå vil avsløre mange feil, som vår Spiller
klassen godtar ikke argumenter når de initialiseres (for nå).
Implementering er veldig grei:
... klasse Spiller attr_accessor: brukernavn inkluderer HTTParty base_uri 'http://api.dribbble.com' def initialiserer (brukernavn) self.username = brukernavn slutt def profil self.class.get "/players/#self.username" avslutte slutt…
Initialiseringsmetoden får et brukernavn som blir lagret inne i klassen takket være attr_accessor
metode lagt til ovenfor. Vi endrer deretter profilmetoden for å interpolere brukernavnetattributtet.
Vi bør få alle våre tester passere igjen.
På et grunnleggende nivå er vår lib i ganske god form. Siden profilen er en Hash, kan vi stoppe her og allerede bruke den ved å sende nøkkelen til attributtet vi vil ha verdien for. Vårt mål er imidlertid å lage en brukervennlig DSL som har en metode for hvert attributt.
La oss tenke på hva vi trenger for å oppnå. La oss anta at vi har en spillereksempel og stubbe hvordan det ville fungere:
player.username => 'simplebits' player.shots_count => 157 player.foo_attribute => NoMethodError
La oss oversette dette til spesifikasjoner og legge dem til GET profil
blokkere:
... må du skrive om "dynamisk attributter" gjør før spilleren.profile avslutter det "må returnere attributtverdien hvis den er til stede i profil" gjør player.id.must_equal 1 avslutte det "må hente metode mangler hvis attributtet ikke er til stede" gjør lambda player. foo_attribute .must_raise NoMethodError slutten ...
Vi har allerede en spesifikasjon for brukernavn, så vi trenger ikke legge til en annen. Legg merke til noen få ting:
player.profile
i en tidligere blokk, ellers vil det være null når vi prøver å få attributtverdien.foo_attribute
oppfordrer et unntak, vi må pakke det inn i en lambda og kontrollere at det øker den forventede feilen.id
er lik 1
, som vi vet at det er den forventede verdien (dette er en rent dataavhengig test).Implementasjonsmessig kunne vi definere en rekke metoder for å få tilgang til profil
hash, men dette ville skape mye duplisert logikk. Videre vil de stole på API-resultatet for alltid å ha de samme nøklene.
"Vi vil stole på
method_missing
å håndtere disse sakene og "generere" alle de metodene som er i gang. "
I stedet vil vi stole på method_missing
å håndtere disse sakene og "generere" alle de metodene som er i gang. Men hva betyr dette? Uten å gå inn i for mye metaprogrammering, kan vi bare si at hver gang vi kaller en metode som ikke er tilstede på objektet, hever Ruby en NoMethodError
ved bruk av method_missing
. Ved å omdefinere denne metoden i en klasse, kan vi endre sin oppførsel.
I vårt tilfelle vil vi fange opp method_missing
ring, bekreft at metodenavnet som er blitt kalt, er en nøkkel i profilen hash, og i tilfelle positivt resultat, returner hashverdien for den aktuelle nøkkelen. Hvis ikke, vil vi ringe super
å heve en standard NoMethodError
: Dette er nødvendig for å sikre at biblioteket vårt oppfører seg akkurat som et annet bibliotek ville gjøre. Med andre ord vil vi garantere minst mulig overraskelse.
La oss legge til følgende kode i Spiller
klasse:
def method_missing (navn, * args, og blokk) hvis profile.has_key? (name.to_s) profil [name.to_s] else super end end
Koden gjør akkurat det som er beskrevet ovenfor. Hvis du kjører spesifikasjonene, bør du få dem alle til å passere. Jeg vil encorage deg å legge til noe mer til spec-filene for noe annet attributt, som shots_count
.
Denne gjennomføringen er imidlertid ikke egentlig idiomatisk Ruby. Det fungerer, men det kan strømlinjeformes til en ternær operatør, en kondensert form av en if-else betinget. Det kan skrives om som:
def method_missing (navn, * args, og blokk) profile.has_key? (name.to_s)? profil [name.to_s]: super end
Det handler ikke bare om lengde, men også om konsistens og felles konvensjoner mellom utviklere. Browsing kildekoden til Ruby edelstener og biblioteker er en god måte å bli vant til disse konvensjonene.
Som et siste skritt vil vi sørge for at vårt bibliotek er effektivt. Det bør ikke gjøre noen flere forespørsler enn nødvendig og muligens cache data internt. Igjen, la oss tenke på hvordan vi kunne bruke det:
player.profile => utfører forespørselen og returnerer en Hash player.profile => returnerer den samme hash player.profile (true) => tvinger omlastingen av http-forespørselen og returnerer hash (med dataendringer om nødvendig)
Hvordan kan vi teste dette? Vi kan ved hjelp av WebMock å aktivere og deaktivere nettverkstilkoblinger til API-endepunktet. Selv om vi bruker VCR-inventar, kan WebMock simulere et tidsavbrudd for nettverket eller et annet svar på serveren. I vårt tilfelle kan vi teste caching ved å få profilen en gang og deretter deaktivere nettverket. Ved å ringe player.profile
igjen bør vi se de samme dataene, mens du ringer player.profile (sann)
vi burde få en Timeout :: Feil
, som biblioteket ville prøve å koble til (deaktivert) API-endepunktet.
La oss legge til en annen blokk til player_spec.rb
fil, rett etter dynamisk attributt generasjon
:
beskrive "caching" gjør # vi bruker Webmock til å deaktivere nettverkstilkoblingen etter at # henter profilen før gjør player.profile stub_request (: any, /api.dribbble.com/).to_timeout avslutte det "må cache profilen" gjør spilleren. profile.must_be_instance_of Hash avslutte det "må oppdatere profilen hvis tvunget" gjør lambda player.profile (true) .must_raise Timeout :: Feil ende ende
De stub_request
Metoden avskjærer alle anrop til API-endepunktet og simulerer en timeout, og øker den forventede Timeout :: Feil
. Som vi gjorde før, tester vi nærværet av denne feilen i en lambda.
Implementering kan være vanskelig, så vi deler den i to trinn. For det første, la oss flytte den faktiske http-forespørselen til en privat metode:
... def profil get_profile end ... privat def get_profile self.class.get ("/ players / # self.username") slutten ...
Dette vil ikke få våre spesifikasjoner passerer, da vi ikke cacherer resultatet av get_profile
. For å gjøre det, la oss endre profil
metode:
... def profil @profile || = get_profile end ...
Vi lagrer resultathash i en instansvariabel. Legg også merke til || =
operatør, hvis tilstedeværelse sørger for at get_profile
kjøres bare hvis @profile returnerer en falsk verdi (som nil
).
Deretter kan vi legge til tvangsdebiteringsdirektivet:
... def profil (kraft = falsk) kraft? @profile = get_profile: @profile || = get_profile end ...
Vi bruker en ternary igjen: hvis makt
er falsk, vi utfører get_profile
og cache det, hvis ikke, bruker vi logikken som er skrevet i den forrige versjonen av denne metoden (dvs. utfører forespørselen bare hvis vi ikke allerede har en hash).
Våre spesifikasjoner skal være grønne nå, og dette er også slutten av vår opplæring.
Vårt mål i denne opplæringen var å skrive et lite og effektivt bibliotek for å samhandle med Dribbble API; Vi har lagt grunnlaget for at dette skal skje. Det meste av logikken vi har skrevet kan abstraheres og gjenbrukes for å få tilgang til alle de andre endepunktene. Minitest, WebMock og VCR har vist seg å være verdifulle verktøy for å hjelpe oss med å forme vår kode.
.