Vroeg of laat moeten alle ontwikkelaars communiceren met een API. Het moeilijkste deel is altijd gerelateerd aan het betrouwbaar testen van de code die we schrijven en omdat we er zeker van willen zijn dat alles naar behoren werkt, voeren we continuosly code uit die de API zelf bevraagt. Dit proces is traag en inefficiënt, omdat we netwerkproblemen en gegevensinconsistenties kunnen ondervinden (de API-resultaten kunnen veranderen). Laten we eens bekijken hoe we dit alles met Ruby kunnen voorkomen.
"Flow is essentieel: schrijf de tests, voer ze uit en zie ze falen, schrijf vervolgens de minimale implementatiecode om ze te laten passeren. Zodra ze allemaal werken, refacteer ze indien nodig."
Ons doel is simpel: schrijf een klein omhulsel rond de Dribbble API om informatie over een gebruiker op te vragen ('player' genoemd in de Dribbble-wereld).
Omdat we Ruby zullen gebruiken, zullen we ook een TDD-aanpak volgen: als je niet bekend bent met deze techniek, heeft Nettuts + een goede primer op RSpec die je kunt lezen. In een notendop zullen we tests schrijven voordat we onze code-implementatie schrijven, waardoor het gemakkelijker wordt om fouten te herkennen en een hoge codekwaliteit te bereiken. Flow is essentieel: schrijf de tests, voer ze uit en zie ze mislukken. Schrijf vervolgens de minimale implementatiecode om ze te laten slagen. Zodra ze allemaal doen, refactor indien nodig.
De Dribbble API is redelijk eenvoudig. Op het moment hiervan ondersteunt het alleen GET-verzoeken en is geen verificatie vereist: een ideale kandidaat voor onze zelfstudie. Bovendien biedt het een limiet van 60 calls per minute, een beperking die perfect laat zien waarom werken met API's een slimme aanpak vereist.
Deze tutorial moet aannemen dat je enige bekendheid hebt met testconcepten: fixtures, mocks, expectations. Testen is een belangrijk onderwerp (vooral in de Ruby-community) en zelfs als u geen Rubyist bent, zou ik u willen aanmoedigen om dieper op de kwestie in te gaan en te zoeken naar equivalente hulpmiddelen voor uw dagelijkse taal. Misschien wilt u "The RSpec book" van David Chelimsky et al., Een uitstekende inleiding over Gedraggedreven ontwikkeling, lezen.
Om het samen te vatten, hier zijn drie kernbegrippen die u moet kennen:
"Voer in het algemeen tests uit telkens wanneer u deze bijwerkt."
WebMock is een Ruby spotbibliotheek die wordt gebruikt om http-verzoeken te bespotten (of te stompen). Met andere woorden, hiermee kunt u elk HTTP-verzoek simuleren zonder er daadwerkelijk een te maken. Het belangrijkste voordeel hiervan is dat het in staat is om tegen elke HTTP-service te ontwikkelen en te testen zonder dat de service zelf nodig is en zonder dat dit gepaard gaat met gerelateerde problemen (zoals API-limieten, IP-beperkingen en dergelijke)..
VCR is een aanvullende tool die elke echte http-aanvraag registreert en een fixture maakt, een bestand dat alle benodigde gegevens bevat om die aanvraag te repliceren zonder deze opnieuw uit te voeren. We zullen het configureren om WebMock te gebruiken om dat te doen. Met andere woorden, onze tests zullen slechts één keer communiceren met de echte Dribbble API: daarna zal WebMock alle verzoeken stomperen dankzij de gegevens die door VCR zijn opgenomen. We zullen een perfecte replica hebben van de Dribbble API-reacties die lokaal zijn opgenomen. WebMock laat ons bovendien randgevallen (zoals de time-out van het verzoek) eenvoudig en consistent testen. Een prachtig gevolg van onze setup is dat alles extreem snel gaat.
Wat het testen van eenheden betreft, zullen we Minitest gebruiken. Het is een snelle en eenvoudige eenheidstestbibliotheek die ook de verwachtingen op de RSpec-manier ondersteunt. Het biedt een kleinere functieset, maar ik merk dat dit je eigenlijk aanmoedigt en duwt om je logica te scheiden in kleine, toetsbare methoden. Minitest is onderdeel van Ruby 1.9, dus als je het gebruikt (ik hoop het wel), hoef je het niet te installeren. Op Ruby 1.8 is het slechts een kwestie van edelsteen installeer minitest
.
Ik zal Ruby 1.9.3 gebruiken: als je dat niet doet, kom je waarschijnlijk een aantal problemen tegen require_relative
, maar ik heb de terugvalcode opgenomen in een opmerking daaronder. Als een algemene praktijk, zou u tests moeten uitvoeren telkens als u hen bijwerkt, zelfs als ik deze stap niet uitdrukkelijk in de zelfstudie zal vermelden.
We zullen de conventionele gebruiken / lib
en / spec
mappenstructuur om onze code te ordenen. Wat betreft de naam van onze bibliotheek, we zullen het noemen Schotel, volgens de Dribbble-conventie van het gebruik van basketbal-gerelateerde termen.
De Gemfile zal al onze afhankelijkheden bevatten, zij het dat ze vrij klein zijn.
bron: rubygems gem 'httparty' group: test do gem 'webmock' gem 'vcr' gem 'zet' gem 'rake' end
Httparty is een makkelijk te gebruiken edelsteen om HTTP-verzoeken af te handelen; het zal de kern van onze bibliotheek zijn. In de testgroep voegen we ook Turn toe om de uitvoer van onze tests te veranderen om meer beschrijvend te zijn en om kleur te ondersteunen.
De / lib
en / spec
mappen hebben een symmetrische structuur: voor elk bestand in de / Lib / schotel
map moet er een bestand in zitten / Spec / schaal
met dezelfde naam en het achtervoegsel '_spec'.
Laten we beginnen met het maken van een /lib/dish.rb
bestand en voeg de volgende code toe:
vereisen "httparty" Dir [File.dirname (__ FILE__) + '/dish/*.rb'].each do | file | bestandseinde vereisen
Het doet niet veel: het vereist 'httparty' en dan wordt het herhaald .rb
bestand binnen / Lib / schotel
om het te eisen. Met dit bestand kunnen we elke functionaliteit in afzonderlijke bestanden toevoegen / Lib / schotel
en laat het automatisch laden door alleen dit enkele bestand te gebruiken.
Laten we naar de / spec
map. Hier is de inhoud van de spec_helper.rb
het dossier.
#we hebben het eigenlijke bibliotheekbestand nodig_relatief '... / lib / gerecht' # Voor 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
Er zijn nogal wat dingen die de moeite waard zijn om op te merken, dus laten we het stukje voor stukje doorbreken:
require_relative
verklaring is een toevoeging van Ruby 1.9.3. minitest / autorun
bevat alle verwachtingen die we zullen gebruiken, webmock / minitest
voegt de benodigde bindingen toe tussen de twee bibliotheken, terwijl videorecorder
en beurt
zijn behoorlijk vanzelfsprekend. Last, but not least, de Rakefile
die een bepaalde ondersteuningscode bevat:
vereisen 'hark / testtask' Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = true end task: default =>: test
De rake / testtask
bibliotheek bevat een TestTask
klasse die nuttig is om de locatie van onze testbestanden in te stellen. Van nu af aan, om onze specificaties uit te voeren, typen we alleen hark
vanuit de hoofdmap van de bibliotheek.
Als een manier om onze configuratie te testen, voegen we de volgende code toe aan /lib/dish/player.rb
:
module Schotelklasse Speler einde
Dan /spec/lib/dish/player_spec.rb
:
require_relative '... / ... / spec_helper' # Voor 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
hardlopen hark
zou u één test voorbij en geen fouten moeten geven. Deze test is zeker niet bruikbaar voor ons project, maar impliciet verifieert het dat onze bibliotheekbestandstructuur aanwezig is (de beschrijven
blok zou een foutmelding geven als het Dish :: Player
module is niet geladen).
Om correct te werken, vereist Dish de Httparty-modules en de juiste base_uri
, d.w.z. de basis-URL van de Dribbble API. Laten we de relevante tests voor deze vereisten in schrijven player_spec.rb
:
... beschrijf Dish :: Player do omschrijf "default attributen" do it "moet bevatten httparty methods" do Dish :: Player.must_include HTTParty end it "moet de base url hebben ingesteld op het Dribble API-endpoint" do Dish :: Player.base_uri .must_equal 'http://api.dribbble.com' end end end
Zoals je ziet, zijn de Minitest-verwachtingen vanzelfsprekend, vooral als je een RSpec-gebruiker bent: het grootste verschil is de formulering, waar Minitest de voorkeur geeft aan "must / wont" tot "should / should_not".
Bij het uitvoeren van deze tests worden één fout en één fout weergegeven. Laat ze onze eerste regels met implementatiecode toevoegen aan deze player.rb
:
module Schotelklasse Speler inclusief HTTParty base_uri 'http://api.dribbble.com' end end
hardlopen hark
opnieuw moet de twee specificaties voorbij zien gaan. Nu onze Speler
klasse heeft toegang tot alle Httparty-klassenmethoden, zoals krijgen
of post
.
Omdat we zullen werken aan de Speler
klasse, we moeten API-gegevens voor een speler hebben. De documentatiepagina van de Dribbble API toont dat het eindpunt om gegevens over een specifieke speler te krijgen is http://api.dribbble.com/players/:id
Zoals bij de typische Rails-mode, :ID kaart
is of het ID kaart of de gebruikersnaam van een specifieke speler. We zullen gebruiken simplebits
, de gebruikersnaam van Dan Cederholm, een van de oprichters van Dribbble.
Om de aanvraag met VCR op te nemen, laten we onze updaten player_spec.rb
bestand door het volgende toe te voegen beschrijven
blok naar de specificatie, direct na de eerste:
... beschrijf "GET-profiel" do before VCR.insert_cassette 'player',: record =>: new_episodes end after do VCR.eject_cassette end it "records the fixture" do Dish :: Player.get ('/ players / simplebits') einde einde
Na het rennen
hark
, je kunt controleren of de fixture is gemaakt. Vanaf nu zullen al onze tests volledig netwerkonafhankelijk zijn.
De voor
blok wordt gebruikt om een specifiek gedeelte van de code uit te voeren vóór elke verwachting: we gebruiken het om de VCR-macro toe te voegen die wordt gebruikt om een fixture op te nemen die we 'speler' zullen noemen. Dit zal een maken player.yml
bestand onder spec / fixtures / dish_cassettes
. De :record
optie is ingesteld om alle nieuwe verzoeken één keer te registreren en ze opnieuw af te spelen bij elk volgend, identiek verzoek. Als proof of concept kunnen we een spec toevoegen waarvan het enige doel is om een fixture op te nemen voor het profiel van simplebits. De na
richtlijn vertelt VCR om de cassette te verwijderen na de tests, en zorg ervoor dat alles goed geïsoleerd is. De krijgen
methode op de Speler
klasse wordt beschikbaar gemaakt, dankzij de opname van de Httparty
module.
Na het rennen hark
, je kunt controleren of de fixture is gemaakt. Vanaf nu zullen al onze tests volledig netwerkonafhankelijk zijn.
Elke Dribbble-gebruiker heeft een profiel dat een vrij uitgebreide hoeveelheid gegevens bevat. Laten we eens nadenken over hoe we zouden willen dat onze bibliotheek echt wordt gebruikt: dit is een handige manier om ons DSL-beleid te laten werken. Dit is wat we willen bereiken:
simplebits = Schotel :: Player.new ('simplebits') simplebits.profile => #retourneert een hash met alle gegevens van de API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157
Eenvoudig en effectief: we willen een speler instantiëren door de gebruikersnaam te gebruiken en vervolgens toegang te krijgen tot zijn gegevens door methoden aan te roepen op de instantie die toewijzen aan de kenmerken die worden geretourneerd door de API. We moeten consistent zijn met de API zelf.
Laten we één ding tegelijk behandelen en een aantal tests schrijven die betrekking hebben op het verkrijgen van de spelersgegevens van de API. We kunnen onze wijzigen "GET-profiel"
blok om te hebben:
beschrijf "GET-profiel" laat (: speler) Dish :: Player.new voordat doen VCR.insert_cassette 'player',: record =>: new_episodes end after do VCR.eject_cassette end it "must have a profile method" do player.must_respond_to: profile end it "moet de api-respons van JSON naar Hash parsen" do player.profile.must_be_instance_of Hash end it "moet het verzoek uitvoeren en de gegevens ophalen" "player.profile [" gebruikersnaam "]. must_equal 'simplebits 'end end
De laat
richtlijn bovenaan maakt een Dish :: Player
bijvoorbeeld beschikbaar in de verwachtingen. Vervolgens willen we ervoor zorgen dat onze speler een profielmethode heeft waarvan de waarde een hash is die de gegevens van de API vertegenwoordigt. Als laatste stap testen we een voorbeeldsleutel (de gebruikersnaam) om er zeker van te zijn dat we het verzoek daadwerkelijk uitvoeren.
Houd er rekening mee dat we nog niet omgaan met het instellen van de gebruikersnaam, omdat dit een volgende stap is. De minimale vereiste implementatie is de volgende:
... class Player include HTTParty base_uri 'http://api.dribbble.com' def profiel self.class.get '/ players / simplebits' end end ...
Een heel klein beetje code: we wikkelen gewoon een get call in de profiel
methode. Vervolgens geven we het hardgecodeerde pad door om de gegevens van simplebits op te halen, gegevens die we al hadden opgeslagen dankzij VCR.
Al onze tests zouden voorbij moeten gaan.
Nu we een werkende profielfunctie hebben, kunnen we voor de gebruikersnaam zorgen. Hier zijn de relevante specificaties:
beschrijf "default instantie attributen" laat (: speler) Dish :: Player.new ('simplebits') it "moet een id attribuut hebben" do player.must_respond_to: gebruikersnaam end it "must have the right id" do speler .username.must_equal 'simplebits' end end beschrijven "GET profile" do let (: player) Dish :: Player.new ('simplebits') before doen VCR.insert_cassette 'base',: record =>: nieuwe_episodes eindigen op do VCR.eject_cassette einde het "moet een profielmethode hebben" do player.must_respond_to: profile end it "moet de api-reactie van JSON naar Hash parseren" do player.profile.must_be_instance_of Hash einde het "moet het juiste profiel krijgen" speler doen .profile ["gebruikersnaam"]. must_equal "simplebits" einde
We hebben een nieuw beschrijvingsblok toegevoegd om de gebruikersnaam die we gaan toevoegen te controleren en simpelweg de speler
initialisatie in de GET profiel
blokkeren om de DSL weer te geven die we willen hebben. Als de specificaties nu worden uitgevoerd, zullen er veel fouten naar voren komen, zoals bij ons Speler
klasse accepteert geen argumenten wanneer geïnitialiseerd (voorlopig).
Implementatie is heel eenvoudig:
... class Player attr_accessor: gebruikersnaam include HTTParty base_uri 'http://api.dribbble.com' def initialize (gebruikersnaam) self.username = gebruikersnaam end def profiel self.class.get "/players/#self.username" einde einde…
De initialisatiemethode krijgt een gebruikersnaam die wordt opgeslagen in de klas dankzij de attr_accessor
methode hierboven toegevoegd. Vervolgens wijzigen we de profielmethode om het gebruikersnaamkenmerk te interpoleren.
We moeten al onze tests opnieuw laten passeren.
Op een basisniveau is ons lib in vrij goede vorm. Omdat profiel een hash is, kunnen we hier stoppen en het al gebruiken door de sleutel van het kenmerk door te geven waarvoor we de waarde willen krijgen. Ons doel is echter om een eenvoudig te gebruiken DSL te maken met een methode voor elk attribuut.
Laten we nadenken over wat we moeten bereiken. Laten we aannemen dat we een spelerinstantie hebben en strobben hoe het zou werken:
player.username => 'simplebits' player.shots_count => 157 player.foo_attribute => NoMethodError
Laten we dit vertalen in specificaties en ze toevoegen aan de GET profiel
blok:
... beschrijven "dynamische attributen" doen voordat speler.profile een einde maakt "moet de attribuutwaarde teruggeven indien aanwezig in profiel" do player.id.must_equal 1 end it "moet methode verhogen als het attribuut niet aanwezig is" do lambda player. foo_attribute .must_raise NoMethodError einde einde ...
We hebben al een spec voor gebruikersnaam, dus we hoeven geen nieuwe toe te voegen. Let op een paar dingen:
player.profile
in een voor blok, anders is het nul als we proberen de attribuutwaarde te krijgen.foo_attribute
veroorzaakt een uitzondering, we moeten deze in een lambda plaatsen en controleren of deze de verwachte fout veroorzaakt.ID kaart
is gelijk aan 1
, omdat we weten dat dit de verwachte waarde is (dit is een puur data-afhankelijke test).Wat de implementatie betreft, kunnen we een reeks methoden definiëren om toegang te krijgen tot de profiel
hash, maar dit zou veel dubbele logica creëren. Bovendien zou het API-resultaat altijd dezelfde sleutels hebben.
"We zullen vertrouwen op
method_missing
om deze gevallen af te handelen en al deze methoden 'on the fly' te genereren. "
In plaats daarvan zullen we vertrouwen op method_missing
om deze gevallen af te handelen en al deze methoden direct te 'genereren'. Maar wat betekent dit? Zonder in te veel metaprogrammering te gaan, kunnen we eenvoudigweg zeggen dat Ruby elke keer dat we een methode noemen die niet aanwezig is op het object, een NoMethodError
door het gebruiken van method_missing
. Door deze methode in een klas opnieuw te definiëren, kunnen we het gedrag ervan wijzigen.
In ons geval zullen we de method_missing
aanroep, controleer of de naam van de methode die is aangeroepen een sleutel is in de hash van het profiel en in het geval van een positief resultaat, de hash-waarde voor die sleutel retourneert. Zo niet, dan bellen we super
om een standaard te verhogen NoMethodError
: dit is nodig om ervoor te zorgen dat onze bibliotheek zich precies gedraagt zoals elke andere bibliotheek dat zou doen. Met andere woorden, we willen de minst mogelijke verrassing garanderen.
Laten we de volgende code toevoegen aan de Speler
klasse:
def method_missing (name, * args, & block) if profile.has_key? (name.to_s) profile [name.to_s] else super end end
De code doet precies wat hierboven is beschreven. Als u nu de specificaties uitvoert, moet u ze allemaal laten passeren. Ik zou je encorage om wat meer toe te voegen aan de spec-bestanden voor een andere eigenschap, zoals shots_count
.
Deze implementatie is echter niet echt idiomatisch Ruby. Het werkt, maar het kan worden gestroomlijnd in een ternaire operator, een gecondenseerde vorm van een if-else conditioneel. Het kan worden herschreven als:
def method_missing (name, * args, & block) profile.has_key? (name.to_s)? profile [name.to_s]: super end
Het is niet alleen een kwestie van lengte, maar ook een kwestie van consistentie en gedeelde conventies tussen ontwikkelaars. Browsing broncode van Ruby edelstenen en bibliotheken is een goede manier om te wennen aan deze conventies.
Als laatste stap willen we ervoor zorgen dat onze bibliotheek efficiënt is. Het zou geen verzoeken meer moeten doen dan nodig en zou intern eventueel gegevens in de cache kunnen opslaan. Nogmaals, laten we nadenken over hoe we het zouden kunnen gebruiken:
player.profile => voert de aanvraag uit en geeft een Hash player terug.profile => retourneert dezelfde hash player.profile (true) => dwingt de reload van de http-aanvraag en retourneert vervolgens de hash (met gegevenswijzigingen indien nodig)
Hoe kunnen we dit testen? We kunnen WebMock gebruiken om netwerkverbindingen met het API-eindpunt in of uit te schakelen. Zelfs als we VCR-fixtures gebruiken, kan WebMock een netwerktime-out of een ander antwoord op de server simuleren. In ons geval kunnen we caching testen door het profiel één keer te verkrijgen en vervolgens het netwerk uit te schakelen. Door te bellen player.profile
opnieuw zouden we dezelfde gegevens moeten zien, terwijl we bellen player.profile (true)
we zouden een moeten krijgen Timeout :: Error
, omdat de bibliotheek zou proberen verbinding te maken met het API-eindpunt (uitgeschakeld).
Laten we nog een blok toevoegen aan de player_spec.rb
bestand, direct erna dynamische attribuutgeneratie
:
beschrijf "caching" do # we gebruiken Webmock om de netwerkverbinding uit te schakelen na # het ophalen van het profiel voordat speler.profile stub_request (: any, /api.dribbble.com/).to_timeout end it "moet het profiel cachen" doen speler. profile.must_be_instance_of Hash eindig het "moet het profiel verversen indien geforceerd" do lambda player.profile (true) .must_raise Timeout :: Error end end
De stub_request
methode onderschept alle aanroepen naar het API-eindpunt en simuleert een time-out, waardoor het verwachte resultaat wordt verhoogd Timeout :: Error
. Zoals we eerder deden, testen we de aanwezigheid van deze fout in een lambda.
Implementatie kan lastig zijn, dus we zullen het opsplitsen in twee stappen. Laten we eerst het eigenlijke http-verzoek verplaatsen naar een privémethode:
... def profiel get_profile einde ... privé def get_profile self.class.get ("/ spelers / # self.username") einde ...
Hierdoor raken onze specificaties niet over, omdat we het resultaat niet bewaren get_profile
. Om dat te doen, laten we de profiel
methode:
... def profiel @profile || = get_profile einde ...
We slaan de resultaat-hash op in een instantievariabele. Let ook op de = ||
operator, wiens aanwezigheid ervoor zorgt dat get_profile
wordt alleen uitgevoerd als @profile een valswaarde retourneert (zoals nul
).
Vervolgens kunnen we de richtlijn voor gedwongen herladen toevoegen:
... def profiel (force = false) kracht? @profile = get_profile: @profile || = get_profile end ...
We gebruiken weer een ternair: als dwingen
is niet waar, we presteren get_profile
en cachegeheugen, zo niet, dan gebruiken we de logica die is geschreven in de vorige versie van deze methode (d.w.z. het uitvoeren van het verzoek alleen als we nog geen hash hebben).
Onze specificaties moeten nu groen zijn en dit is ook het einde van onze tutorial.
Ons doel in deze tutorial was om een kleine en efficiënte bibliotheek te schrijven om te communiceren met de Dribbble API; we hebben de basis gelegd voor dit gebeuren. De meeste logica die we hebben geschreven, kan worden geabstraheerd en opnieuw worden gebruikt om toegang te krijgen tot alle andere eindpunten. Minitest, WebMock en VCR hebben bewezen waardevolle hulpmiddelen te zijn om ons te helpen onze code vorm te geven.
.