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.
"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.
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.
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:
"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.
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:
require_relative
uttalandet är en Ruby 1.9.3-tillägg. 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. 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ö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
.
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.
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.
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.
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:
player.profile
i ett tidigare block, annars kommer det att vara noll när vi försöker få attributvärdet.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.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.
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.
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.
.