Skriva en API Wrapper i Ruby med TDD

Förr eller senare måste alla utvecklare interagera med ett API. Den svåraste delen är alltid relaterad till att på ett tillförlitligt sätt testa koden vi skriver och, eftersom vi vill se till att allt fungerar ordentligt, kör vi kontinuerligt kod som frågar API själv. Denna process är långsam och ineffektiv, eftersom vi kan uppleva nätverksproblem och datainstyrkningar (API-resultaten kan ändras). Låt oss se hur vi kan undvika all denna insats med Ruby.


Vårt mål

"Flöde är viktigt: skriv testen, springa dem och se dem misslyckas, skriv sedan den minsta implementeringskoden för att få dem att passera. När de alla gör det, om det behövs."

Vårt mål är enkelt: skriv ett litet omslag runt Dribbble API för att hämta information om en användare (kallad "spelare" i Dribble-världen).
Eftersom vi kommer att använda Ruby, följer vi också ett TDD-tillvägagångssätt: Om du inte är bekant med den här tekniken har Nettuts + en bra primer på RSpec som du kan läsa. I ett nötskal ska vi skriva tester innan du skriver vår kodimplementering, vilket gör det enklare att upptäcka fel och att uppnå en högkodskvalitet. Flöde är viktigt: skriv testen, kör dem och se dem misslyckas, skriv sedan den minsta implementeringskoden för att få dem att passera. När de alla gör, refaktor om det behövs.

API: n

Dribbble API är ganska enkelt. Vid det här tillfället stöder det bara GET-förfrågningar och kräver inte autentisering: en idealisk kandidat för vår handledning. Dessutom erbjuder den en 60 gräns per minut, en begränsning som perfekt visar varför att arbeta med API: er kräver ett smart tillvägagångssätt.


Nyckelbegrepp

Denna handledning måste anta att du har viss förtrogenhet med testkoncept: fixtures, mocks, expectations. Testning är ett viktigt ämne (särskilt i Ruby-samhället) och även om du inte är en rubyist, skulle jag uppmuntra dig att gräva djupare in i frågan och söka efter motsvarande verktyg för ditt vardagsspråk. Du kanske vill läsa "The RSpec-boken" av David Chelimsky et al., En utmärkt primer om beteendedriven utveckling.

För att sammanfatta här är här tre nyckelbegrepp som du måste veta:

  • Falsk: även kallad dubbel, en mock är "ett objekt som står för ett annat objekt i ett exempel". Det betyder att om vi vill testa samspelet mellan ett objekt och ett annat kan vi mocka den andra. I denna handledning kommer vi att mocka Dribbble API, för att testa vår kod som vi inte behöver API, själv, men något som beter sig som det och exponerar samma gränssnitt.
  • Fixtur: en dataset som återskapar ett visst tillstånd i systemet. En fixtur kan användas för att skapa den nödvändiga data för att testa en bit av logik.
  • Förväntan: Ett testexempel skrivet det ur resultatet av resultatet vi vill uppnå.

Våra verktyg

"Som en allmän praxis kör test varje gång du uppdaterar dem."

WebMock är ett Ruby mocking-bibliotek som används för att mocka (eller stubba) http-förfrågningar. Med andra ord kan du simulera någon HTTP-begäran utan att faktiskt göra en. Den främsta fördelen med detta är att kunna utveckla och testa mot någon HTTP-tjänst utan att behöva själva tjänsten och utan att ådra sig i relaterade problem (som API-gränser, IP-restriktioner och liknande).
VCR är ett komplementärt verktyg som registrerar någon riktig http-förfrågan och skapar en fixtur, en fil som innehåller all nödvändig data för att replikera den begäran utan att utföra den igen. Vi kommer att konfigurera det för att använda WebMock för att göra det. Med andra ord, våra test kommer att interagera med det riktiga Dribbble API bara en gång: efter det kommer WebMock stubba alla önskemål tack vare data inspelad av videobandspelare. Vi kommer att få en perfekt replik av Dribbble API-svaren inspelade lokalt. Dessutom får WebMock oss enkelt och konsekvent att testa kantfall (som förfrågningsutskottet). En underbar konsekvens av vår inställning är att allt kommer att bli extremt snabbt.

När det gäller enhetstestning använder vi Minitest. Det är ett snabbt och enkelt enhetstestbibliotek som också stöder förväntningarna i RSpec-mode. Det erbjuder en mindre funktionalitet men jag tycker att det faktiskt uppmuntrar och driver dig att skilja din logik till små testbara metoder. Minitest är en del av Ruby 1.9, så om du använder det (jag hoppas det) behöver du inte installera det. På Ruby 1.8 är det bara en fråga om gem install minitest.

Jag ska använda Ruby 1.9.3: om du inte gör det kommer du förmodligen att stöta på några problem relaterade till require_relative, men jag har inkluderat återgångskoden i en kommentar precis nedanför den. Som en allmän praxis borde du köra test varje gång du uppdaterar dem, även om jag inte kommer att nämna detta steg explicit i hela handledningen.


Inrätta

Vi kommer att använda den konventionella / lib och / spec mappstruktur för att organisera vår kod. När det gäller namnet på vårt bibliotek kallar vi det Maträtt, efter Dribbble-konventionen om att använda basketrelaterade termer.

Gemfile kommer att innehålla alla våra beroende, om än de är ganska små.

 källa: rubygems pärla 'httparty' grupp: test gör pärla 'webmock' pärla 'vcr' pärla 'sväng' pärla 'rake' slutet

Httparty är en enkel att använda pärla för att hantera HTTP-förfrågningar; Det kommer att vara kärnan i vårt bibliotek. I testgruppen kommer vi också att lägga till Vrid för att ändra utmatningen från våra tester för att vara mer beskrivande och för att stödja färg.

De / lib och / spec mappar har en symmetrisk struktur: för varje fil som finns i / Lib / skål mapp, det borde finnas en fil inuti / Spec / skål med samma namn och suffixet _special.

Låt oss börja med att skapa en /lib/dish.rb fil och lägg till följande kod:

 kräver "httparty" Dir [Fil.dirname (__ FILE__) + '/dish/*.rb'].each do | file | kräver filändamål

Det gör inte mycket: det kräver 'httparty' och sedan iterates över alla .rb fil inuti / Lib / skål att kräva det. Med den här filen på plats kommer vi att kunna lägga till någon funktionalitet i separata filer i / Lib / skål och laddar den automatiskt genom att bara behöva den här filen.

Låt oss flytta till / spec mapp. Här är innehållet i spec_helper.rb fil.

 #Vi behöver den faktiska 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 finns en hel del saker här värda att notera, så låt oss bryta den bit för bit:

  • Först kräver vi huvud lib-filen för vår app, vilket gör koden vi vill testa tillgängliga för testpaketet. De require_relative uttalandet är en Ruby 1.9.3-tillägg.
  • Vi behöver då alla bibliotekets beroenden: MINITEST / autorun innehåller alla förväntningar vi ska använda, webmock / MINITEST lägger till de nödvändiga bindningarna mellan de två biblioteken, medan videobandspelare och sväng är ganska självförklarande.
  • Konfigurationsskärmen för sväng behöver bara justera vår testutgång. Vi kommer att använda konturformatet, där vi kan se beskrivningen av våra specifikationer.
  • VCR-konfigurationsblocken berättar videobandspelare för att lagra förfrågningarna i en fixturmapp (notera den relativa sökvägen) och använda WebMock som ett stubbigt bibliotek (videobandspelare stöder andra).

Sist men inte minst, Rakefile som innehåller viss supportkod:

 kräver "rake / testtask" Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = true end-uppgift: default =>: test

De rake / testtask biblioteket innehåller a TestTask klass som är användbar för att ställa in platsen för våra testfiler. Från och med nu, för att köra våra specifikationer, skriver vi bara räfsa från bibliotekets rotkatalog.

Som ett sätt att testa vår konfiguration, lägger vi till följande kod till /lib/dish/player.rb:

 modul Skålklass Spelarens änd ände

Sedan /spec/lib/dish/player_spec.rb:

 require_relative '... / ... / spec_helper' # För 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öpning räfsa bör ge dig ett prov som passerar och inga fel. Detta test är inte alls användbart för vårt projekt, men det verifierar implicit att vår biblioteksfilstruktur är på plats ( beskriva block skulle kasta ett fel om Dish :: Player modulen laddades inte).


Första Specs

För att fungera korrekt kräver skålen Httparty-modulerna och det korrekta base_uri, d.v.s. basadressen för Dribbble API. Låt oss skriva relevanta tester för dessa krav i player_spec.rb:

... beskriv disken :: Spelaren beskriver "standard attribut" gör det "måste innehålla httparty metoder" gör disken :: Player.must_include HTTParty avsluta den "måste ha basadressen inställd till Dribble API-ändpunkten" göra disken :: Player.base_uri .must_equal 'http://api.dribbble.com' slutet slutet

Som du kan se är Minitest förväntningar självförklarande, speciellt om du är en RSpec-användare: den största skillnaden är formulering, där Minitest föredrar "måste / brukar" till "bör / bör inte".

Om du kör dessa tester visas ett fel och ett fel. För att få dem att passera, låt oss lägga till våra första rader av implementeringskoden till player.rb:

 Modul Dish Class Player inkluderar HTTParty base_uri 'http://api.dribbble.com' slutet

Löpning räfsa igen bör visa de två specifikationerna som passerar. Nu vår Spelare klassen har tillgång till alla Httparty klassmetoder, som skaffa sig eller posta.


Inspelning av vår första förfrågan

Som vi kommer att arbeta på Spelare klass måste vi ha API-data för en spelare. Dokumentationssidan för Dribbble API visar att slutpunkten för att få data om en viss spelare är http://api.dribbble.com/players/:id

Som i typiska Rails mode, : id är antingen id eller den Användarnamn av en specifik spelare. Vi kommer att använda simplebits, Användarnamnet för Dan Cederholm, en av Dribbble-grundarna.

För att spela in förfrågan med videobandspelare, låt oss uppdatera vår player_spec.rb fil genom att lägga till följande beskriva blockera till spec, strax efter den första:

... beskriva "GET-profil" gör innan VCR.insert_cassette 'player',: record =>: new_episodes avslutar efter att VCR.eject_cassette avslutar det "registrerar fixturen" gör Dish :: Player.get ('/ players / simplebits') ändänden

Efter körning räfsa, Du kan kontrollera att fixturen har skapats. Hittills kommer alla våra test att vara helt nätverksoberoende.

De innan block används för att exekvera en viss del av kod före varje förväntan: vi använder den för att lägga till videobandspelaren som används för att spela in en fixtur som vi kommer att kalla "spelare". Detta skapar en player.yml fil under spec / fixturer / dish_cassettes. De :spela in alternativet är inställt för att spela in alla nya förfrågningar en gång och spela om dem på varje efterföljande, identisk förfrågan. Som ett bevis på konceptet kan vi lägga till en specifikation vars enda syfte är att spela in en fixtur för simplebits profil. De efter Direktivet berättar för videobandspelaren att ta bort kassetten efter provningarna, se till att allt är ordentligt isolerat. De skaffa sig metod på Spelare klassen görs tillgänglig tack vare införandet av Httparty modul.

Efter körning räfsa, Du kan kontrollera att fixturen har skapats. Hittills kommer alla våra test att vara helt nätverksoberoende.


Hämta spelarprofilen

Varje Dribbble-användare har en profil som innehåller en ganska stor mängd data. Låt oss tänka på hur vi vill att vårt bibliotek ska vara när det faktiskt används: Det här är en användbar metod för att klä ut våra DSL-datorer. Här är vad vi vill uppnå:

 simplebits = Skål :: Player.new ('simplebits') simplebits.profile => #retrar en hash med all data från API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157

Enkelt och effektivt: Vi vill inställa en spelare genom att använda sitt användarnamn och sedan få tillgång till dess data genom att anropa metoder på förekomsten som kartan till attributen som returneras av API. Vi måste vara konsekventa med API själv.

Låt oss ta itu med en sak åt gången och skriva några tester relaterade till att få spelardata från API: n. Vi kan ändra vår "Få profil" blockera att ha:

 beskriva "GET-profilen" gör (: spelare) Skål :: Player.new innan VCR.insert_cassette 'player',: record =>: new_episodes sluta efter slutar VCR.eject_cassette det "måste ha en profilmetod" player.must_respond_to: profil avsluta "måste analysera api svaret från JSON till Hash" gör player.profile.must_be_instance_of Hash avsluta det "måste utföra förfrågan och få data" göra player.profile ["användarnamn"]. måste_ekvivalenta "simplebits "änden

De låta Direktivet på toppen skapar en Dish :: Player Exempel finns i förväntningarna. Därefter vill vi se till att vår spelare har en profilmetod vars värde är en hash som representerar data från API: n. Som ett sista steg testar vi en provnyckel (användarnamnet) för att se till att vi faktiskt utför begäran.

Observera att vi ännu inte hanterar hur du anger användarnamnet, eftersom detta är ett ytterligare steg. Den minimala genomförandet som krävs är följande:

... klassspelare inkluderar HTTParty base_uri 'http://api.dribbble.com' def profil self.class.get '/ players / simplebits' slutet ... 

En mycket liten mängd koden: vi packar bara ett fåtal i profil metod. Vi skickar sedan den hårdkodade sökvägen för att hämta simplebits data, data som vi redan lagrat tack vare videobandspelare.

Alla våra tester borde passera.


Ställa in användarnamnet

Nu när vi har en fungerande profilfunktion kan vi ta hand om användarnamnet. Här är de relevanta specifikationerna:

 beskriv "standard instans attribut" gör leta (: spelare) Skål :: Player.new ('simplebits') det måste ha ett id-attribut "do player.must_respond_to: användarnamn avsluta det" måste ha rätt id " .username.must_equal "simplebits" -änden beskriver "GET-profil" gör (: spelare) Skålen :: Player.new ('simplebits') innan VCR.insert_cassette 'base',: record =>: new_episodes slutar efter gör VCR.eject_cassette det "måste ha en profilmetod" gör player.must_respond_to: profil avsluta "måste analysera api-svaret från JSON till Hash" gör player.profile.must_be_instance_of Hash avsluta det "måste få rätt profil" gör spelare .profile ["användarnamn"]. must_equal "simplebits" änden

Vi har lagt till ett nytt beskrivningsblock för att kontrollera användarnamnet vi ska lägga till och helt enkelt ändra spelare initialisering i GET-profil blockera för att återspegla DSL vi vill ha. Att köra specifikationerna nu kommer att avslöja många fel, som vår Spelare klassen accepterar inte argument när de initialiseras (för nu).

Genomförandet är väldigt enkelt:

... klass Spelare attr_accessor: användarnamn inkluderar HTTParty base_uri 'http://api.dribbble.com' def initiera (användarnamn) self.username = användarnamn slut def profile self.class.get "/players/#self.username" avsluta slutet… 

Initialiseringsmetoden får ett användarnamn som lagras inne i klassen tack vare attr_accessor metod tillsatt ovan. Vi ändrar sedan profildetoden för att interpolera användarnamnattributet.

Vi borde få alla våra test att gå igen.


Dynamiska attribut

På en grundläggande nivå är vår lib i ganska bra form. Eftersom profilen är en Hash, kunde vi stanna här och använda den redan genom att skicka nyckeln till attributet som vi vill få värdet för. Vårt mål är dock att skapa en enkel att använda DSL som har en metod för varje attribut.

Låt oss tänka på vad vi behöver uppnå. Låt oss anta att vi har en spelare förekomst och stöta på hur det skulle fungera:

 player.username => 'simplebits' player.shots_count => 157 player.foo_attribute => NoMethodError

Låt oss översätta detta till specs och lägga till dem i GET-profil blockera:

... beskriv "dynamiska attribut" gör innan do player.profile avsluta "måste returnera attributvärdet om det finns i profil" gör player.id.must_equal 1 avsluta det "måste hämta metod saknas om attributet inte är närvarande" gör lambda player. foo_attribute .must_raise NoMethodError slutänden ... 

Vi har redan en spec för användarnamn, så vi behöver inte lägga till en annan. Observera några saker:

  • vi kallar uttryckligen player.profile i ett tidigare block, annars kommer det att vara noll när vi försöker få attributvärdet.
  • att testa det foo_attribute väcker ett undantag, vi måste sätta i det i en lambda och kontrollera att det ökar det förväntade felet.
  • vi testar det id är lika med 1, som vi vet att det är det förväntade värdet (detta är ett rent databeroende test).

Implementeringsvis kan vi definiera en rad metoder för att komma åt profil hash, men detta skulle skapa en hel del duplicerad logik. Dessutom skulle de förlita sig på API-resultatet att alltid ha samma nycklar.

"Vi kommer att lita på method_missing att hantera dessa fall och "generera" alla de metoder som finns i flygningen. "

Istället kommer vi att lita på method_missing att hantera dessa fall och "generera" alla de metoder som finns i flygningen. Men vad betyder det här? Utan att gå in på för mycket metaprogrammering kan vi helt enkelt säga att varje gång vi kallar en metod som inte finns på objektet, höjer Ruby en NoMethodError genom att använda method_missing. Genom att omdefiniera denna metod i en klass kan vi ändra sitt beteende.

I vårt fall kommer vi att fånga upp method_missing ring, verifiera att det metodnamn som har kallats är en nyckel i profilhashen och returnera ish-värdet för den nyckeln om det finns ett positivt resultat. Om inte, vi ringer super att höja en standard NoMethodError: Detta behövs för att säkerställa att vårt bibliotek beter sig precis som ett annat bibliotek skulle göra. Med andra ord vill vi garantera minsta möjliga överraskning.

Låt oss lägga till följande kod till Spelare klass:

 def method_missing (namn, * args, och block) om profile.has_key? (name.to_s) profil [name.to_s] else super end end

Koden gör exakt vad som beskrivs ovan. Om du nu kör specifikationerna, ska du få dem alla att passera. Jag skulle encorage dig att lägga till lite mer till spec-filerna för något annat attribut, som shots_count.

Denna implementering är emellertid inte egentligen idiomatisk Ruby. Det fungerar, men det kan strömlinjeformas till en ternär operatör, en kondenserad form av en om-annars villkorlig. Det kan skrivas om som:

 def method_missing (namn, * args, och block) profile.has_key? (name.to_s)? profil [name.to_s]: super end

Det handlar inte bara om längd utan också om konsistens och gemensamma konventioner mellan utvecklare. Browsing källkod av Ruby ädelstenar och bibliotek är ett bra sätt att bli van vid dessa konventioner.


caching

Som ett sista steg vill vi se till att vårt bibliotek är effektivt. Det borde inte göra några fler förfrågningar än det behövs och eventuellt cache data internt. Låt oss återigen tänka på hur vi skulle kunna använda det:

 player.profile => utför begäran och returnerar en Hash player.profile => returnerar samma hash player.profile (true) => tvingar omlastningen av http-förfrågan och returnerar sedan hash (med dataändringar om det behövs)

Hur kan vi testa detta? Vi kan med hjälp av WebMock att aktivera och inaktivera nätverksanslutningar till API-ändpunkten. Även om vi använder videobandspelare kan WebMock simulera ett nätverkstidsutbyte eller ett annat svar på servern. I vårt fall kan vi testa cachning genom att få profilen en gång och sedan inaktivera nätverket. Genom att ringa player.profile igen bör vi se samma data, samtidigt som vi ringer player.profile (true) vi borde få en Timeout :: Error, som biblioteket skulle försöka ansluta till (slut) API-ändpunkten.

Låt oss lägga till ett annat block till player_spec.rb fil, strax efter dynamisk attributgenerering:

 beskriv # caching profile.must_be_instance_of Hash avsluta "måste uppdatera profilen om den är tvungen" gör lambda player.profile (true) .must_raise Timeout :: Feländänd

De stub_request Metoden avlyssnar alla samtal till API-ändpunkten och simulerar en timeout, vilket höjer den förväntade Timeout :: Error. Som vi gjorde tidigare testar vi förekomsten av detta fel i en lambda.

Implementering kan vara svårt, så vi delar upp det i två steg. För det första, låt oss flytta den faktiska http-förfrågan till en privat metod:

... def profil get_profile end ... privat def get_profile self.class.get ("/ players / # self.username") slutet ... 

Detta kommer inte att få våra specifikationer att passera, eftersom vi inte cachar resultatet av get_profile. För att göra det, låt oss ändra profil metod:

... def profil @profile || = get_profile end ... 

Vi lagrar resultathashen i en instansvariabel. Observera också || = operatör, vars närvaro ser till att get_profile körs endast om @profile returnerar ett falskt värde (som noll).

Därefter kan vi lägga till det tvingade omlastningsdirektivet:

... def profil (kraft = falsk) kraft? @profile = get_profile: @profile || = get_profile end ... 

Vi använder en ternary igen: om tvinga är falskt, vi utför get_profile och cache det, om inte, använder vi logiken som skrivits i den tidigare versionen av den här metoden (dvs. utför endast begäran om vi inte redan har en hash).

Våra specifikationer ska vara gröna nu och det här är också slutet på vår handledning.


Avslutar

Vårt syfte i denna handledning var att skriva ett litet och effektivt bibliotek för att interagera med Dribbble API; Vi har lagt grunden för att detta ska hända. Det mesta av logiken vi har skrivit kan abstraheras och återanvändas för att komma åt alla andra ändpunkter. Minitest, WebMock och videobandspelare har visat sig vara värdefulla verktyg för att hjälpa oss att forma vår kod.

.