Skriver en API Wrapper i Ruby med TDD

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.


Vårt mål

"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.

API

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.


Nøkkelkonsepter

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:

  • håne: også kalt dobbel, en mock er "et objekt som står for et annet objekt i et eksempel". Dette betyr at hvis vi vil teste samspillet mellom et objekt og en annen, kan vi spotte den andre. I denne opplæringen vil vi spotte Dribbble API, for å teste koden vi ikke trenger API, selv, men noe som oppfører seg som det, og viser det samme grensesnittet.
  • ligaen: et datasett som gjenskaper en bestemt tilstand i systemet. En fixtur kan brukes til å skape de nødvendige dataene for å teste et logikkstykke.
  • Forventning: Et testeksempel skrevet fra det synspunktet av resultatet vi ønsker å oppnå.

Våre verktøy

"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.


Setup

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:

  • Først trenger vi hovedfilen for vår app, og koden vi vil teste tilgjengelig for testpakken. De require_relative erklæring er en Ruby 1.9.3 tillegg.
  • Vi krever da alle bibliotekets avhengigheter: 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.
  • Konfigurasjonsblokken for sving må bare justere testutgangen. Vi vil bruke oversiktsformatet, der vi kan se beskrivelsen av våre spesifikasjoner.
  • VCR-konfigureringsblokkene forteller videospilleren for å lagre forespørslene i en fixturmappe (merk av den relative banen) og å bruke WebMock som et stubbebibliotek (VCR støtter noen andre).

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).


Første spesifikasjoner

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 eller post.


Opptak av vår første forespørsel

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 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.


Å få spillerprofilen

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.


Angi brukernavnet

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.


Dynamiske egenskaper

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:

  • vi kaller eksplisitt player.profile i en tidligere blokk, ellers vil det være null når vi prøver å få attributtverdien.
  • for å teste det foo_attribute oppfordrer et unntak, vi må pakke det inn i en lambda og kontrollere at det øker den forventede feilen.
  • vi tester det 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.


caching

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.


Wrapping Up

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.

.