Mai devreme sau mai târziu, toți dezvoltatorii trebuie să interacționeze cu un API. Partea cea mai dificilă este întotdeauna legată de testarea fiabilă a codului pe care îl scriem și, pe măsură ce ne asigurăm că totul funcționează corect, continuăm rularea codului care interoghează API-ul în sine. Acest proces este lent și ineficient, deoarece putem întâmpina probleme de rețea și inconsecvențe de date (rezultatele API se pot schimba). Să analizăm modul în care putem evita toate aceste eforturi cu Ruby.
"Fluxul este esențial: scrieți testele, executați-le și vedeți-le că nu reușesc, apoi scrieți codul minim de implementare pentru a le face să treacă. După ce fac toate, refactorul dacă este necesar."
Scopul nostru este simplu: scrieți un mic pachet în jurul API-ului Dribbble pentru a obține informații despre un utilizator (numit "player" în lumea Dribbble).
Pe măsură ce vom folosi Ruby, vom urma și o abordare TDD: dacă nu sunteți familiarizat cu această tehnică, Nettuts + are un primer bun pe RSpec pe care îl puteți citi. Pe scurt, vom scrie teste înainte de a scrie implementarea codului nostru, făcând mai ușor detectarea de erori și obținerea unei calități înalte a codului. Fluxul este esențial: scrieți testele, executați-le și vedeți-le că nu reușesc, apoi scrieți codul de implementare minim pentru a le face să treacă. Odată ce toți fac, refactor dacă este necesar.
API-ul Dribbble este destul de simplu. În momentul de față, acesta acceptă numai cererile GET și nu necesită autentificare: un candidat ideal pentru tutorialul nostru. În plus, oferă o limită de 60 de apeluri pe minut, o restricție care arată perfect de ce lucrul cu API necesită o abordare inteligentă.
Acest tutorial trebuie să presupună că aveți o anumită familiaritate cu conceptele de testare: programări, machete, așteptări. Testarea este un subiect important (mai ales în comunitatea Ruby) și chiar dacă nu sunteți un Rubyist, v-aș încuraja să explorați mai profund problema și să căutați instrumente echivalente pentru limba dvs. de zi cu zi. Poate doriți să citiți "Cartea RSpec" de David Chelimsky și colaboratorii, un excelent primer pe dezvoltarea comportamentului comportamental.
Pentru a rezuma aici, aici sunt trei concepte-cheie pe care trebuie să le cunoașteți:
"Ca o practică generală, executați teste de fiecare dată când le actualizați."
WebMock este o bibliotecă de batjocură Ruby, care este folosită pentru a derula (sau stub) cererile HTTP. Cu alte cuvinte, vă permite să simulați orice solicitare HTTP fără să faceți una. Principalul avantaj al acestui lucru este dezvoltarea și testarea împotriva oricărui serviciu HTTP fără a avea nevoie de serviciul însuși și fără a se supune unor probleme conexe (cum ar fi limitele API, restricțiile IP și altele asemenea).
VCR-ul este un instrument complementar care înregistrează orice cerere adevărată de http și creează un fișier, un fișier care conține toate datele necesare pentru a replica acea solicitare fără a o efectua din nou. O vom configura astfel încât să folosească WebMock pentru a face acest lucru. Cu alte cuvinte, testele noastre vor interacționa cu API-ul real Dribbble doar o singură dată: după aceea, WebMock va împiedica toate solicitările datorită datelor înregistrate de VCR. Vom avea o replică perfectă a răspunsurilor Dribbble API înregistrate la nivel local. În plus, WebMock ne va permite să analizăm cu ușurință și în mod consecvent cazurile de testare (cum ar fi expirarea cererii). O consecință minunată a configurației noastre este că totul va fi extrem de rapid.
În ceea ce privește testarea unităților, vom folosi Minitest. Este o bibliotecă rapidă și simplă de testare a unităților, care suportă și așteptările în modul RSpec. Oferă un set de caracteristici mai mici, dar consider că acest fapt vă încurajează și vă împinge să vă separați logica de metodele mici, verificabile. Minitest face parte din Ruby 1.9, deci dacă îl folosiți (sper), nu trebuie să îl instalați. Pe Ruby 1.8, este doar o problemă gem instala minitest
.
Voi folosi Ruby 1.9.3: dacă nu, veți întâlni probabil unele probleme legate de require_relative
, dar am inclus codul de rezervă într-un comentariu chiar sub el. Ca o practică generală, ar trebui să executați teste de fiecare dată când le actualizați, chiar dacă nu voi menționa acest pas explicit în tutorial.
Vom folosi sistemul convențional / lib
și / spec
pentru a organiza codul nostru. În ceea ce privește numele bibliotecii noastre, o vom numi Farfurie, în urma convenției Dribbble privind utilizarea termenilor legați de baschet.
Gemfilele vor conține toate dependențele noastre, deși sunt destul de mici.
sursa: rubygems gem 'httparty' grup: test do gem 'webmock' gem 'vcr' gem 'turn' gem
Httparty este o gem ușor de folosit pentru a gestiona cererile HTTP; acesta va fi nucleul bibliotecii noastre. În grupul de testare, vom adăuga, de asemenea, rândul său, pentru a schimba rezultatele testelor noastre pentru a fi mai descriptive și pentru a susține culoarea.
/ lib
și / spec
folderele au o structură simetrică: pentru fiecare fișier din fișierul / Lib / vas
dosar, ar trebui să existe un fișier înăuntru / Spec / vas
cu același nume și sufixul "_spec".
Să începem prin crearea unui /lib/dish.rb
fișier și adăugați următorul cod:
cereți "httparty" Dir [File.dirname (__FILE__) + '/dish/*.rb'].each do | file | necesită sfârșitul fișierului
Nu face prea mult: necesită "httparty" și apoi iterează peste fiecare .rb
fișier înăuntru / Lib / vas
să o solicite. Cu acest fișier în loc, vom putea adăuga orice funcționalitate în interiorul fișierelor separate în / Lib / vas
și să o încărcați automat doar prin solicitarea acestui singur fișier.
Să trecem la / spec
pliant. Iată conținutul spec_helper.rb
fişier.
#we nevoie de fișierul bibliotecii actual requ_relative '... / lib / dish' # Pentru 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
Sunt multe lucruri care merită notate, așa că haideți să o despărțim cu bucățică:
require_relative
declarația este o adăugare Ruby 1.9.3. minitest / Autorun
include toate așteptările pe care le vom folosi, webmock / minitest
adaugă legăturile necesare între cele două biblioteci, în timp ce vcr
și viraj
sunt destul de explicative. Nu în ultimul rând, Rakefile
care conține un cod de asistență:
cereți rake / testtask Rake :: TestTask.new nu | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = adevărat final task: default =>: test
grebla / testtask
biblioteca include a TestTask
clasa care este utilă pentru a seta locația fișierelor noastre de testare. De acum înainte, pentru a rula specificațiile noastre, vom scrie doar greblă
din directorul rădăcină al bibliotecii.
Ca o modalitate de a testa configurația noastră, să adăugăm următorul cod /lib/dish/player.rb
:
Modul Clasă de mâncare Sfârșitul final al jucătorului
Atunci /spec/lib/dish/player_spec.rb
:
requ_relative '... / ... / spec_helper' # Pentru 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
Alergare greblă
ar trebui să vă dau un test de trecere și nici o eroare. Acest test nu este deloc util pentru proiectul nostru, dar verifică implicit faptul că structura noastră de fișiere bibliotecă este în vigoare descrie
bloc ar arunca o eroare în cazul în care Dish :: Jucător
modulul nu a fost încărcat).
Pentru a funcționa corect, antena necesită modulele Httparty și cele corecte ului de bază
, adică urlul de bază al API Dribbble. Să scriem testele relevante pentru aceste cerințe în player_spec.rb
:
... descrie Dish :: Jucătorul nu descrie "atributele implicite" nu trebuie să includă metode httparty "do Dish :: Player.must_include HTTParty end it" trebuie să aibă urlul de bază setat la finalul API Dribble "do Dish :: Player.base_uri .must_equal "http://api.dribbble.com" capăt sfârșit sfârșit
După cum puteți vedea, așteptările Minitest sunt explicite, mai ales dacă sunteți un utilizator RSPE: cea mai mare diferență este formularea, unde Minitest preferă "must / wont" la "should / should_not".
Rularea acestor teste va afișa o eroare și un eșec. Pentru a le trece, să adăugăm primele linii de cod de implementare player.rb
:
modulul Dish class Player include HTTParty base_uri 'http://api.dribbble.com' sfârșitul final
Alergare greblă
din nou ar trebui să arate cele două specificații care trec. Acum Jucător
clasa are acces la toate metodele din clasa Httparty, cum ar fi obține
sau post
.
Așa cum vom lucra la Jucător
, vom avea nevoie de date API pentru un jucător. Pagina de documentare API Dribbble indică faptul că punctul final pentru obținerea de date despre un anumit player este http://api.dribbble.com/players/:id
La fel ca în stilul tipic Rails, : id
este fie id sau nume de utilizator de un anumit jucător. Vom folosi simplebits
, numele de utilizator al lui Dan Cederholm, unul dintre fondatorii Dribbble.
Pentru a înregistra cererea cu VCR, să ne actualizăm player_spec.rb
fișier prin adăugarea următoarelor descrie
blocați la spec., imediat după primul:
... descrieți "Profilul GET" înainte de a face VCR.insert_cassette 'player',: record =>: new_episodes se termină după ce VCR.eject_cassette termină "înregistrează fișierul" do Dish :: Player.get ('/ players / simplebits' sfârșitul capătului final
După ce a alergat
greblă
, puteți verifica dacă dispozitivul a fost creat. De acum încolo, toate testele noastre vor fi complet independente de rețea.
inainte de
bloc este folosit pentru a executa o anumită porțiune de cod înainte de orice așteptare: îl folosim pentru a adăuga macro-ul VCR folosit pentru a înregistra un dispozitiv pe care îl vom numi "player". Acest lucru va crea o player.yml
fișier în spec / dispozitive de fixare / dish_cassettes
. :record
opțiunea este setată să înregistreze toate solicitările noi o dată și să le redea pe fiecare solicitare ulterioară, identică. Ca o dovadă a conceptului, putem adăuga un spec al cărui singur scop este să înregistreze un dispozitiv pentru profilul lui simplebits. după
directiva îi spune VCR-ului să scoată caseta după teste, asigurându-se că totul este izolat corespunzător. obține
metoda pe Jucător
clasa este pusă la dispoziție, datorită includerii Httparty
modul.
După ce a alergat greblă
, puteți verifica dacă dispozitivul a fost creat. De acum încolo, toate testele noastre vor fi complet independente de rețea.
Fiecare utilizator Dribbble are un profil care conține o cantitate destul de mare de date. Sa ne gandim la modul in care dorim ca biblioteca noastra sa fie folosita de fapt: aceasta este o modalitate utila de a ne desfasura activitatile noastre DSL. Iată ce vrem să obținem:
simplebits = Dish :: Player.new ('simplebits') simplebits.profile => #returns un hash cu toate datele din API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157
Simplu și eficient: vrem să instanțiăm un Player utilizând numele său de utilizator și să obținem accesul la datele sale prin apelarea metodelor pe instanța care se potrivește cu atributele returnate de API. Trebuie să fim în concordanță cu API în sine.
Să abordăm un singur lucru la un moment dat și să scriem câteva teste referitoare la obținerea datelor din API. Ne putem modifica "GET profilul"
bloc pentru a avea:
descrieți "Profilul GET" nu permite (: player) Dish :: Player.new înainte de a face VCR.insert_cassette 'player',: record =>: new_episodes se termină după ce VCR.eject_cassette încheie "trebuie să aibă o metodă de profil" player.must_respond_to: sfarsitul profilului "trebuie sa parseze raspunsul api de la JSON la hash" face player.profile.must_be_instance_of Hash sa termine "trebuie sa execute cererea si sa obtina datele" face player.profile ["username"] must_equal 'simplebits "sfârșitul final
lăsa
directivă în partea de sus creează o Dish :: Jucător
exemplu disponibile în așteptări. Apoi, vrem să ne asigurăm că jucătorul nostru are o metodă de profil a cărei valoare este un hash reprezentând datele din API. Ca ultim pas, testăm o cheie de probă (numele de utilizator) pentru a ne asigura că executăm efectiv solicitarea.
Rețineți că nu gestionăm încă cum să setăm numele de utilizator, deoarece acesta este un pas suplimentar. Implementarea minimă necesară este următoarea:
... player-ul de clasă include HTTParty base_uri 'http://api.dribbble.com' def profile self.class.get '/ players / end-end simplebits' ...
O cantitate foarte mică de cod: pur și simplu înfășurăm un apel în profil
metodă. Apoi trecem calea hardcoded pentru a prelua datele simplebits, date pe care le-am stocat deja datorită VCR-ului.
Toate testele noastre ar trebui să treacă.
Acum, că avem o funcție de profil de lucru, putem avea grijă de numele de utilizator. Iată specificațiile relevante:
descrieți "atributele de instanță implicite" nu permite (: player) Dish :: Player.new ('simplebits') trebuie să aibă un atribut id 'face player.must_respond_to: nume de utilizator final' trebuie să aibă ID-ul corect ' .username.must_equal sfârșitul sfârșitului "simplebits" descrie "GET profil" do let (: player) Dish :: Player.new ('simplebits') înainte de a face VCR.insert_cassette 'base',: record =>: new_episodes end after face VCR.eject_cassette terminați-l "trebuie să aibă o metodă de profil" face player.must_respond_to: profilul trebuie să parseze răspunsul api de la JSON la Hash " .profile ["username"]. must_equal "simplebits" sfârșitul final
Am adăugat un nou bloc de descriere pentru a verifica numele de utilizator pe care îl vom adăuga și pur și simplu modificați jucător
inițializare în Profilul GET
bloc pentru a reflecta DSL pe care dorim să avem. Rularea specificațiilor va dezvălui acum multe erori, ca și noi Jucător
clasa nu acceptă argumente când este inițializată (pentru moment).
Implementarea este foarte simplă:
... class Player attr_accessor: numele de utilizator include HTTParty base_uri 'http://api.dribbble.com' def initialize (username) self.username = nume utilizator end end profil self.class.get "/players/#self.username" end Sfârșit…
Metoda de inițializare obține un nume de utilizator care este stocat în interiorul clasei datorită attr_accessor
adăugată mai sus. Apoi, schimbăm metoda profilului pentru a interpola atributul de nume de utilizator.
Ar trebui să trecem din nou toate testele.
La un nivel de bază, lib este într-o formă destul de bună. Deoarece profilul este o Hash, am putea să ne oprim aici și să îl folosim deja prin trecerea cheii atributului pentru care vrem să obținem valoarea. Scopul nostru, totuși, este de a crea un DSL ușor de folosit, care are o metodă pentru fiecare atribut.
Să ne gândim la ceea ce trebuie să realizăm. Să presupunem că avem un exemplu de jucător și cum ar fi funcționat:
player.username => 'simplebits' player.shots_count => 157 player.foo_attribute => NoMethodError
Să traducem acest lucru în specificații și să le adăugăm la Profilul GET
bloc:
... descrie "atributele dinamice" face înainte ca player.profile să se termine "trebuie să returneze valoarea atributului dacă este prezentă în profil" do player.id.must_equal 1 sfârșitul "trebuie să ridice metoda lipsă dacă atributul nu este prezent" do lambda player. foo_attribute .must_raise NoMethodError sfârșitul final ...
Avem deja o spec. Pentru numele de utilizator, deci nu mai trebuie să adăugăm altul. Rețineți câteva lucruri:
player.profile
într-un bloc anterior, altfel va fi zero atunci când încercăm să obținem valoarea atributului.foo_attribute
ridică o excepție, trebuie să-l înfășurăm într-o lambda și să verificăm că ridică eroarea așteptată.id
este egală 1
, deoarece știm că aceasta este valoarea așteptată (acesta este un test pur dependent de date).Din punct de vedere al implementării, am putea defini o serie de metode pentru a accesa profil
hash, dar acest lucru ar crea o mulțime de logică duplicat. În plus, se va baza pe rezultatul API pentru a avea întotdeauna aceleași chei.
"Ne vom baza pe asta
method_missing
să se ocupe de aceste cazuri și să "genereze" toate aceste metode în zbor. "
În schimb, ne vom baza method_missing
să se ocupe de aceste cazuri și să "genereze" toate aceste metode în zbor. Dar ce înseamnă asta? Fără a intra în prea multă metaprogramare, putem spune pur și simplu că de fiecare dată când numim o metodă care nu este prezentă pe obiect, Ruby ridică NoMethodError
prin utilizarea method_missing
. Prin redefinirea acestei metode în interiorul unei clase, putem modifica comportamentul acesteia.
În cazul nostru, vom intercepta method_missing
apelați, verificați dacă numele metodei care a fost apelat este o cheie în hash-ul profilului și, în cazul unui rezultat pozitiv, returnați valoarea hash pentru cheia respectivă. Dacă nu, vom apela super
pentru a ridica un standard NoMethodError
: aceasta este necesară pentru a ne asigura că biblioteca noastră se comportă exact așa cum ar face orice altă bibliotecă. Cu alte cuvinte, dorim să garantăm cea mai mică surpriză posibilă.
Să adăugăm următorul cod la Jucător
clasă:
def method_missing (nume, * args, & block) dacă profilul profile.has_key? (nume.to_s) [nume.to_s] altceva sfârșitul super-final
Codul face exact ceea ce este descris mai sus. Dacă rulați acum specificațiile, ar trebui să le treceți toți. Vă încurajez să adăugați ceva mai mult la fișierele spec. Pentru alt atribut, cum ar fi shots_count
.
Această implementare, cu toate acestea, nu este Ruby cu adevărat idiomatică. Funcționează, dar poate fi raționalizată într-un operator ternar, o formă condensată a unei condiționări dacă este altfel. Poate fi rescris ca:
def metoda_missing (nume, * args, & block) profil.has_key? (name.to_s)? profil [name.to_s]: super-sfârșit
Nu este doar o problemă de lungime, ci și o chestiune de coerență și convenții comune între dezvoltatori. Navigarea codului sursă al bijuteriilor și bibliotecilor Ruby este o modalitate bună de a vă obișnui cu aceste convenții.
Ca ultim pas, vrem să ne asigurăm că biblioteca noastră este eficientă. Nu ar trebui să facă mai multe cereri decât este necesar și, eventual, să cacheze datele intern. Încă o dată, să ne gândim cum am putea să o folosim:
player.profile => execută cererea și returnează un player Hash.profile => returnează același hash player.profile (true) => forțează reîncărcarea solicitării http și apoi returnează hash-ul (cu modificările de date, dacă este necesar)
Cum putem testa acest lucru? Putem folosi WebMock pentru a activa și dezactiva conexiunile de rețea la punctul final API. Chiar dacă folosim corpuri de date VCR, WebMock poate simula un timeout al rețelei sau un răspuns diferit la server. În cazul nostru, putem testa cache-ul prin obținerea profilului o dată și apoi dezactivarea rețelei. Sunând player.profile
din nou ar trebui să vedem aceleași date, în timp ce sunăm player.profile (true)
ar trebui să primim a Timeout :: Eroare
, deoarece biblioteca ar încerca să se conecteze la punctul final API (dezactivat).
Să adăugăm un alt bloc la player_spec.rb
dosar, imediat după generarea atributului dinamic
:
descrieți "caching" nu # vom folosi Webmock pentru a dezactiva conexiunea la rețea după # # preluarea profilului înainte de a face player.profile stub_request (: orice, /api.dribbble.com/).to_timeout sfârșitul "trebuie să memoreze profilul" face player. profile.must_be_instance_of Hash sfârșitul "trebuie să actualizeze profilul dacă este forțat" do lambda player.profile (true) .must_raise Timeout :: Sfârșit sfârșitul erorii
stub_request
metoda interceptează toate apelurile către punctul final al API și simulează un timeout, ridicând așteptările Timeout :: Eroare
. Așa cum am făcut înainte, testăm prezența acestei erori într-o lambda.
Implementarea poate fi dificilă, așa că o vom împărți în două etape. În primul rând, hai să mutăm cererea efectivă http la o metodă privată:
... def defineste profilul get_profile ... private def get_profile self.class.get ("/ players / # self.username") sfarsit ...
Acest lucru nu va face ca specificatiile noastre sa treaca, pentru ca nu vom cachea rezultatul get_profile
. Pentru a face asta, hai să schimbăm profil
metodă:
... def profile @profile || = sfârșitul get_profile ...
Vom stoca rezultatul hash într-o variabilă de instanță. De asemenea, rețineți || =
operator, a cărui prezență asigură acest lucru get_profile
se execută numai dacă @profile returnează o valoare falsă (cum ar fi zero
).
Apoi putem adăuga directiva de reîncărcare forțată:
... def profil (Forța = falsă) forță? @profile = get_profile: @profile || = sfârșitul get_profile ...
Folosim un ternar din nou: dacă forta
este falsă, realizăm get_profile
și cache-ul, dacă nu, folosim logica scrisă în versiunea anterioară a acestei metode (adică efectuarea cererii numai dacă nu avem deja hash).
Specificatiile noastre ar trebui sa fie verzi acum si acest lucru este si sfarsitul tutorialului nostru.
Scopul nostru în acest tutorial a fost să scriem o bibliotecă mică și eficientă pentru a interacționa cu API-ul Dribbble; am pus temelia ca acest lucru să se întâmple. Majoritatea logicii pe care am scris-o pot fi abstracte și reutilizate pentru a accesa toate celelalte puncte finale. Minitest, WebMock și VCR s-au dovedit a fi instrumente valoroase pentru a ne ajuta să ne formăm codul.
.