Scrivere un wrapper API in Ruby con TDD

Prima o poi, tutti gli sviluppatori devono interagire con un'API. La parte più difficile è sempre legata alla verifica attendibile del codice che scriviamo e, poiché vogliamo assicurarci che tutto funzioni correttamente, eseguiamo continuamente codice che interroga l'API stessa. Questo processo è lento e inefficiente, in quanto possiamo riscontrare problemi di rete e incoerenze nei dati (i risultati dell'API potrebbero cambiare). Esaminiamo come possiamo evitare tutto questo sforzo con Ruby.


Il nostro obbiettivo

"Il flusso è essenziale: scrivere i test, eseguirli e vederli fallire, quindi scrivere il codice di implementazione minimo per farli passare. Una volta che tutti lo fanno, refactor se necessario."

Il nostro obiettivo è semplice: scrivere un piccolo wrapper attorno all'API di Dribbble per recuperare informazioni su un utente (chiamato "player" nel mondo di Dribbble).
Dato che useremo Ruby, seguiremo anche un approccio TDD: se non hai familiarità con questa tecnica, Nettuts + ha un buon primer su RSpec che puoi leggere. In poche parole, scriveremo dei test prima di scrivere la nostra implementazione del codice, rendendo più facile individuare i bug e ottenere una buona qualità del codice. Il flusso è essenziale: scrivere i test, eseguirli e vederli fallire, quindi scrivere il codice di implementazione minimo per farli passare. Una volta che tutti lo fanno, refactoring se necessario.

L'API

L'API di Dribbble è abbastanza semplice. Al momento supporta solo le richieste GET e non richiede l'autenticazione: un candidato ideale per il nostro tutorial. Inoltre, offre un limite di 60 chiamate al minuto, una restrizione che mostra perfettamente il motivo per cui lavorare con le API richiede un approccio intelligente.


Concetti chiave

Questo tutorial deve presupporre di avere una certa dimestichezza con i concetti di testing: fixture, mock, aspettative. Il test è un argomento importante (specialmente nella comunità Ruby) e anche se non sei un Rubyist, ti incoraggio a approfondire la questione ea cercare strumenti equivalenti per il tuo linguaggio quotidiano. Si consiglia di leggere "Il libro di RSpec" di David Chelimsky et al., Un eccellente primer su Behavior Driven Development.

Per riassumere qui, ecco tre concetti chiave che devi conoscere:

  • finto: chiamato anche double, un mock è "un oggetto che rappresenta un altro oggetto in un esempio". Ciò significa che se vogliamo testare l'interazione tra un oggetto e un altro, possiamo prendere in giro il secondo. In questo tutorial, prenderemo in giro l'API di Dribbble, in quanto per testare il nostro codice non abbiamo bisogno dell'API stessa, ma qualcosa che si comporta come tale ed espone la stessa interfaccia.
  • infisso: un set di dati che ricrea uno stato specifico nel sistema. Un dispositivo può essere utilizzato per creare i dati necessari per testare un pezzo di logica.
  • aspettativa: un esempio di prova scritto dal punto di vista del risultato che vogliamo raggiungere.

I nostri strumenti

"Come pratica generale, esegui i test ogni volta che li aggiorni."

WebMock è una libreria di derisione Ruby che viene utilizzata per simulare (o stub) richieste HTTP. In altre parole, ti permette di simulare qualsiasi richiesta HTTP senza realizzarne una. Il vantaggio principale di questo è essere in grado di sviluppare e testare qualsiasi servizio HTTP senza richiedere il servizio stesso e senza incorrere in problemi correlati (come limiti API, restrizioni IP e simili).
Il videoregistratore è uno strumento complementare che registra qualsiasi richiesta http reale e crea una fixture, un file che contiene tutti i dati necessari per replicare quella richiesta senza eseguirla nuovamente. Lo configureremo per utilizzare WebMock per farlo. In altre parole, i nostri test interagiranno con la vera API di Dribbble solo una volta: dopo di ciò, WebMock interromperà tutte le richieste grazie ai dati registrati dal videoregistratore. Avremo una replica perfetta delle risposte dell'API Dribbble registrate localmente. Inoltre, WebMock ci consentirà di testare i casi limite (come la scadenza della richiesta) in modo semplice e coerente. Una meravigliosa conseguenza del nostro setup è che tutto sarà estremamente veloce.

Per quanto riguarda i test unitari, useremo Minitest. È una libreria di test di unità rapida e semplice che supporta anche le aspettative nella moda RSpec. Offre un set di funzionalità più piccolo, ma trovo che questo in realtà ti incoraggi e ti spinga a separare la tua logica in piccoli metodi testabili. Minitest fa parte di Ruby 1.9, quindi se lo stai usando (lo spero) non è necessario installarlo. Su Ruby 1.8, è solo una questione di gem installazione minitest.

Userò Ruby 1.9.3: se non lo fai, probabilmente incontrerai alcuni problemi relativi a require_relative, ma ho incluso il codice di fallback in un commento subito sotto. Come pratica generale, devi eseguire i test ogni volta che li aggiorni, anche se non menzionerò questo passaggio esplicitamente durante l'esercitazione.


Impostare

Useremo il convenzionale / lib e / spec struttura delle cartelle per organizzare il nostro codice. Per quanto riguarda il nome della nostra biblioteca, lo chiameremo Piatto, seguendo la convenzione di Dribbble sull'uso dei termini relativi al basket.

Il Gemfile conterrà tutte le nostre dipendenze, anche se sono piuttosto piccole.

 fonte: rubygems gem 'httparty' group: test fai gemma 'webmock' gemma 'vcr' gemma 'gira' gemma 'rastrello' fine

Httparty è un gioi facile da usare per gestire le richieste HTTP; sarà il cuore della nostra biblioteca. Nel gruppo di test, aggiungeremo anche Turn per cambiare l'output dei nostri test per essere più descrittivi e per supportare il colore.

Il / lib e / spec le cartelle hanno una struttura simmetrica: per ogni file contenuto nel / Lib / piatto cartella, ci dovrebbe essere un file all'interno / Spec / piatto con lo stesso nome e il suffisso '_spec'.

Iniziamo creando un /lib/dish.rb file e aggiungi il seguente codice:

 richiede "httparty" Dir [File.dirname (__ FILE__) + '/dish/*.rb'].each do | file | richiede la fine del file

Non fa molto: richiede 'httparty' e poi scorre su ogni .rb file all'interno / Lib / piatto per richiederlo. Con questo file in posizione, saremo in grado di aggiungere qualsiasi funzionalità all'interno di file separati in / Lib / piatto e lo fanno caricare automaticamente solo richiedendo questo singolo file.

Passiamo al / spec cartella. Ecco il contenuto del spec_helper.rb file.

 #we bisogno del file della libreria reale require_relative '... / lib / dish' # Per 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

Ci sono alcune cose qui che vale la pena notare, quindi cerchiamo di romperlo pezzo per pezzo:

  • All'inizio, abbiamo bisogno del file di lib principale per la nostra app, rendendo disponibile il codice che vogliamo testare alla suite di test. Il require_relative la dichiarazione è un'aggiunta di Ruby 1.9.3.
  • Richiediamo quindi tutte le dipendenze della libreria: Minitest / autorun include tutte le aspettative che useremo, webmock / Minitest aggiunge i collegamenti necessari tra le due librerie, mentre videoregistratore e turno sono abbastanza auto-esplicativi.
  • Il blocco di configurazione Turn ha solo bisogno di modificare l'output del test. Useremo il formato del contorno, dove possiamo vedere la descrizione delle nostre specifiche.
  • I blocchi di configurazione del videoregistratore indicano a VCR di memorizzare le richieste in una cartella fixture (notare il percorso relativo) e di utilizzare WebMock come libreria di stub (il videoregistratore ne supporta altre).

Ultimo, ma non meno importante, il Rakefile che contiene qualche codice di supporto:

 richiede 'rake / testtask' Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = true end task: default =>: test

Il rake / testtask la biblioteca include a TestTask classe che è utile per impostare la posizione dei nostri file di test. D'ora in poi, per eseguire le nostre specifiche, scriveremo solo rastrello dalla directory radice della libreria.

Per testare la nostra configurazione, aggiungiamo il seguente codice a /lib/dish/player.rb:

 modulo Piatto classe Player end end

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

 require_relative '... / ... / spec_helper' # Per 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

In esecuzione rastrello dovrebbe darti un passaggio di prova e nessun errore. Questo test non è assolutamente utile per il nostro progetto, ma verifica implicitamente che la struttura del nostro file libreria sia a posto (il descrivere il blocco genererebbe un errore se il Piatto :: Player il modulo non è stato caricato).


Prime specifiche

Per funzionare correttamente, Dish richiede i moduli Httparty e il corretto base_uri, cioè l'url di base dell'API di Dribbble. Scriviamo i test rilevanti per questi requisiti in player_spec.rb:

... descrivi Dish :: Player descrivi "attributi predefiniti" fallo "deve includere i metodi httparty" do Dish :: Player.must_include HTTParty terminalo "deve avere l'url base impostato sull'endpoint API Dribble" do Dish :: Player.base_uri .must_equal 'http://api.dribbble.com' end end end

Come puoi vedere, le attese Minitest sono autoesplicative, specialmente se sei un utente RSpec: la più grande differenza è la dicitura, dove Minitest preferisce "must / wont" a "should / should_not".

L'esecuzione di questi test mostrerà un errore e un errore. Per farli passare, aggiungiamo le nostre prime linee di codice di implementazione a player.rb:

 Il modulo Dish class Player include HTTParty base_uri 'http://api.dribbble.com' end end

In esecuzione rastrello di nuovo dovrebbe mostrare le due specifiche che passano. Ora il nostro Giocatore la classe ha accesso a tutti i metodi della classe Httparty, come ottenere o inviare.


Registrare la nostra prima richiesta

Come lavoreremo su Giocatore classe, avremo bisogno di avere dati API per un giocatore. La pagina di documentazione dell'API di Dribbble mostra che l'endpoint per ottenere dati su un giocatore specifico è http://api.dribbble.com/players/:id

Come nella tipica moda Rails, : id è o il id o il nome utente di un giocatore specifico. Useremo SimpleBits, il nome utente di Dan Cederholm, uno dei fondatori di Dribbble.

Per registrare la richiesta con il videoregistratore, aggiorniamo il nostro player_spec.rb file aggiungendo quanto segue descrivere blocco alla specifica, subito dopo la prima:

... descrivere "GET profile" prima di fare VCR.insert_cassette 'player',: record =>: new_episodes end dopo do VCR.eject_cassette termina "registra la fixture" fai Dish :: Player.get ('/ players / simplebits') fine fine

Dopo aver corso rastrello, puoi verificare che l'apparecchiatura sia stata creata. D'ora in poi, tutti i nostri test saranno completamente indipendenti dalla rete.

Il prima il blocco viene utilizzato per eseguire una porzione specifica di codice prima di ogni aspettativa: lo usiamo per aggiungere la macro VCR utilizzata per registrare un dispositivo che chiameremo "player". Questo creerà a player.yml file sotto spec / fixtures / dish_cassettes. Il :disco l'opzione è impostata per registrare tutte le nuove richieste una volta e riprodurle su ogni richiesta successiva, identica. Come prova del concetto, possiamo aggiungere una specifica il cui unico scopo è registrare un dispositivo per il profilo di un bitmap. Il dopo la direttiva dice al VCR di rimuovere la cassetta dopo i test, assicurandosi che tutto sia correttamente isolato. Il ottenere metodo sul Giocatore la classe è resa disponibile, grazie all'inclusione del Httparty modulo.

Dopo aver corso rastrello, puoi verificare che l'apparecchiatura sia stata creata. D'ora in poi, tutti i nostri test saranno completamente indipendenti dalla rete.


Ottenere il profilo del giocatore

Ogni utente Dribbble ha un profilo che contiene una quantità piuttosto ampia di dati. Pensiamo a come vorremmo che fosse la nostra biblioteca quando viene effettivamente utilizzata: questo è un modo utile per concretizzare il funzionamento della nostra DSL. Ecco cosa vogliamo ottenere:

 simplebits = Dish :: Player.new ('simplebits') simplebits.profile => # restituisce un hash con tutti i dati dell'API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157

Semplice ed efficace: vogliamo creare un'istanza di un giocatore utilizzando il suo nome utente e ottenere l'accesso ai suoi dati chiamando i metodi sull'istanza che si associano agli attributi restituiti dall'API. Dobbiamo essere coerenti con l'API stessa.

Affrontiamo una cosa alla volta e scriviamo alcuni test relativi all'ottenimento dei dati del giocatore dall'API. Possiamo modificare il nostro "OTTIENI profilo" blocco da avere:

 descrivi "GET profile" fai (: player) Dish :: Player.new prima di fare VCR.insert_cassette 'player',: record =>: new_episodes end dopo do VCR.eject_cassette e termina "deve avere un metodo profile" do player.must_respond_to: profile end it "deve analizzare la risposta api da JSON a Hash" do player.profile.must_be_instance_of Hash terminarlo "deve eseguire la richiesta e ottenere i dati" do player.profile ["username"]. must_equal ' 'end end

Il permettere la direttiva in alto crea a Piatto :: Player istanza disponibile nelle aspettative. Successivamente, vogliamo assicurarci che il nostro giocatore abbia un metodo profilo il cui valore è un hash che rappresenta i dati dell'API. Come ultimo passaggio, testiamo una chiave di esempio (il nome utente) per assicurarci che effettivamente eseguiamo la richiesta.

Si noti che non stiamo ancora gestendo come impostare il nome utente, in quanto si tratta di un ulteriore passaggio. L'implementazione minima richiesta è la seguente:

... class player include HTTParty base_uri 'http://api.dribbble.com' profilo def self.class.get '/ players / simplebits' end end ... 

Una piccola quantità di codice: stiamo solo completando una chiamata get in profilo metodo. Passiamo quindi il percorso hardcoded per recuperare i dati di simplebits, dati che avevamo già archiviato grazie a VCR.

Tutti i nostri test dovrebbero passare.


Impostazione del nome utente

Ora che abbiamo una funzione di profilo operativo, possiamo occuparci del nome utente. Ecco le specifiche pertinenti:

 descrivi "attributi di istanza predefiniti" do let (: player) Dish :: Player.new ('simplebits') it "deve avere un attributo id" do player.must_respond_to: username end it "deve avere l'id giusto" do player .username.must_equal 'simplebits' end end descrive "GET profile" do let (: player) Dish :: Player.new ('simplebits') prima di fare VCR.insert_cassette 'base',: record =>: new_episodes terminano dopo fai VCR.eject_cassette e termina "deve avere un metodo di profilo" do player.must_respond_to: profile end it "deve analizzare la risposta api da JSON a Hash" do player.profile.must_be_instance_of Hash termina "deve ottenere il profilo giusto" fare player .profile ["username"]. must_equal "simplebits" end end

Abbiamo aggiunto un nuovo blocco descrittivo per controllare il nome utente che stiamo per aggiungere e semplicemente modificato giocatore inizializzazione in Ottieni il profilo blocco per riflettere il DSL che vogliamo avere. Esecuzione delle specifiche ora rivelerà molti errori, come il nostro Giocatore la classe non accetta argomenti se inizializzata (per ora).

L'implementazione è molto semplice:

... class Player attr_accessor: username include HTTParty base_uri 'http://api.dribbble.com' def initialize (nome utente) self.username = nome utente end def profilo self.class.get "/players/#self.username" fine fine… 

Il metodo di inizializzazione ottiene un nome utente che viene memorizzato all'interno della classe grazie a attr_accessor metodo aggiunto sopra. Modifichiamo quindi il metodo profile per interpolare l'attributo username.

Dovremmo fare passare tutti i nostri test ancora una volta.


Attributi dinamici

A un livello base, la nostra lib è in ottima forma. Poiché il profilo è un hash, potremmo fermarci qui e usarlo già passando la chiave dell'attributo per cui vogliamo ottenere il valore. Il nostro obiettivo, tuttavia, è creare un DSL facile da usare che abbia un metodo per ciascun attributo.

Pensiamo a ciò che dobbiamo raggiungere. Supponiamo di avere un'istanza di giocatore e di stub come funzionerebbe:

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

Trasformiamo questo in specifiche e aggiungiamolo al Ottieni il profilo bloccare:

... descrivi "attributi dinamici" fai prima che player.profile finisca "deve restituire il valore dell'attributo se presente nel profilo" do player.id.must_equal 1 e termina "deve aumentare il metodo se l'attributo non è presente" do lambda player. foo_attribute .must_raise NoMethodError end end ... 

Abbiamo già una specifica per il nome utente, quindi non è necessario aggiungerne un'altra. Nota alcune cose:

  • chiamiamo esplicitamente player.profile in un blocco precedente, altrimenti sarà nullo quando tenteremo di ottenere il valore dell'attributo.
  • per testarlo foo_attribute solleva un'eccezione, dobbiamo avvolgerla in un lambda e verificare che aumenti l'errore previsto.
  • lo testiamo id è uguale a 1, come sappiamo che questo è il valore atteso (questo è un test puramente dipendente dai dati).

Per quanto riguarda l'implementazione, potremmo definire una serie di metodi per accedere a profilo hash, tuttavia ciò creerebbe molta logica duplicata. Inoltre, si farebbe affidamento sul risultato API per avere sempre le stesse chiavi.

"Ci faremo affidamento method_missing per gestire questi casi e 'generare' tutti quei metodi al volo. "

Invece, faremo affidamento su method_missing per gestire questi casi e "generare" tutti quei metodi al volo. Ma cosa significa? Senza entrare troppo nella metaprogrammazione, possiamo semplicemente dire che ogni volta che chiamiamo un metodo non presente sull'oggetto, Ruby solleva un NoMethodError usando method_missing. Ridefinendo questo stesso metodo all'interno di una classe, possiamo modificare il suo comportamento.

Nel nostro caso, intercetteremo il method_missing chiamare, verificare che il nome del metodo che è stato chiamato sia una chiave nell'hash del profilo e, in caso di risultato positivo, restituire il valore hash per quella chiave. In caso contrario, chiameremo super alzare uno standard NoMethodError: questo è necessario per assicurarsi che la nostra libreria si comporti esattamente come farebbe qualsiasi altra libreria. In altre parole, vogliamo garantire la minima sorpresa possibile.

Aggiungiamo il seguente codice al Giocatore classe:

 def method_missing (name, * args, & block) if profile.has_key? (name.to_s) profile [name.to_s] else super end end

Il codice fa esattamente quello descritto sopra. Se ora esegui le specifiche, dovresti farle passare tutte. Ti incoraggerei ad aggiungerne altri ai file spec per altri attributi, come shots_count.

Questa implementazione, tuttavia, non è veramente un rubino idiomatico. Funziona, ma può essere snellito in un operatore ternario, una forma condensata di un condizionale if else. Può essere riscritto come:

 def method_missing (name, * args, & block) profile.has_key? (name.to_s)? profilo [nome.to_s]: super fine

Non è solo una questione di lunghezza, ma anche una questione di coerenza e convenzioni condivise tra gli sviluppatori. Navigare nel codice sorgente delle gemme e librerie Ruby è un buon modo per abituarsi a queste convenzioni.


caching

Come passo finale, vogliamo essere sicuri che la nostra biblioteca sia efficiente. Non dovrebbe fare più richieste del necessario e probabilmente memorizzare dati internamente. Ancora una volta, pensiamo a come potremmo usarlo:

 player.profile => esegue la richiesta e restituisce un hash player.profile => restituisce lo stesso hash player.profile (true) => forza il ricaricamento della richiesta http e quindi restituisce l'hash (con le modifiche dei dati se necessario)

Come possiamo testarlo? Possiamo utilizzare WebMock per abilitare e disabilitare le connessioni di rete all'endpoint API. Anche se utilizziamo proiettori VCR, WebMock può simulare un timeout di rete o una diversa risposta al server. Nel nostro caso, possiamo testare il caching ottenendo il profilo una volta e poi disabilitando la rete. A chiamata player.profile ancora dovremmo vedere gli stessi dati, mentre chiamando player.profile (vero) dovremmo ottenere un Timeout :: Errore, poiché la libreria proverà a connettersi all'endpoint API (disabilitato).

Aggiungiamo un altro blocco al player_spec.rb file, subito dopo generazione di attributi dinamici:

 descrivi "caching" do # usiamo Webmock per disabilitare la connessione di rete dopo aver # recuperato il profilo prima di fare player.profile stub_request (: any, /api.dribbble.com/).to_timeout terminarlo "deve memorizzare nella cache il profilo" fare player. profile.must_be_instance_of Hash termina "deve aggiornare il profilo se forzato" do lambda player.profile (true) .must_raise Timeout :: Errore end end

Il stub_request il metodo intercetta tutte le chiamate all'endpoint API e simula un timeout, aumentando l'attesa Timeout :: Errore. Come abbiamo fatto prima, testiamo la presenza di questo errore in una lambda.

L'implementazione può essere complicata, quindi la suddivideremo in due passaggi. Innanzitutto, spostiamo la richiesta HTTP effettiva su un metodo privato:

... def profilo get_profile end ... private def get_profile self.class.get ("/ players / # self.username") end ... 

Questo non farà passare le nostre specifiche, dato che non stiamo memorizzando il risultato get_profile. Per farlo, cambiamo il profilo metodo:

... profilo def @profile || = get_profile fine ... 

Memorizzeremo l'hash risultato in una variabile di istanza. Si noti anche il || = operatore, la cui presenza lo assicura get_profile viene eseguito solo se @profile restituisce un valore falsy (come zero).

Successivamente possiamo aggiungere la direttiva di ricarica forzata:

... profilo di forza (forza = falso) forza? @profile = get_profile: @profile || = get_profile fine ... 

Stiamo usando di nuovo un ternario: se vigore è falso, ci esibiamo get_profile e mettilo in cache, altrimenti, usiamo la logica scritta nella versione precedente di questo metodo (cioè eseguendo la richiesta solo se non abbiamo già un hash).

Le nostre specifiche dovrebbero essere verdi ora e questa è anche la fine del nostro tutorial.


Avvolgendo

Il nostro scopo in questo tutorial era scrivere una piccola ed efficiente libreria per interagire con l'API di Dribbble; abbiamo gettato le basi per questo. La maggior parte della logica che abbiamo scritto può essere astratta e riutilizzata per accedere a tutti gli altri endpoint. Minitest, WebMock e VCR si sono dimostrati strumenti preziosi per aiutarci a modellare il nostro codice.

.