() translation by (you can also view the original English article)
Früher oder später müssen alle Entwickler mit einer API interagieren. Der schwierigste Teil bezieht sich immer auf das zuverlässige Testen des von uns geschriebenen Codes. Da wir sicherstellen möchten, dass alles ordnungsgemäß funktioniert, führen wir kontinuierlich Code aus, der die API selbst abfragt. Dieser Prozess ist langsam und ineffizient, da Netzwerkprobleme und Dateninkonsistenzen auftreten können (die API-Ergebnisse können sich ändern). Sehen wir uns an, wie wir all diese Anstrengungen mit Ruby vermeiden können.
Unser Ziel
"Der Ablauf ist wichtig: Schreiben Sie die Tests, führen Sie sie aus und sehen Sie, dass sie fehlschlagen. Schreiben Sie dann den minimalen Implementierungscode, damit sie erfolgreich sind. Wenn alle dies tun, überarbeiten Sie sie bei Bedarf."
Unser Ziel ist einfach: Schreiben Sie einen kleinen Wrapper um die Dribbble-API, um Informationen über einen Benutzer abzurufen (in der Dribbble-Welt als "Player" bezeichnet). Da wir Ruby verwenden werden, werden wir auch einen TDD-Ansatz verfolgen: Wenn Sie mit dieser Technik nicht vertraut sind, verfügt Nettuts+ über eine gute Grundierung für RSpec, die Sie lesen können. Kurz gesagt, wir werden Tests schreiben, bevor wir unsere Code-Implementierung schreiben, um Fehler zu erkennen und eine hohe Codequalität zu erzielen. Der Ablauf ist wichtig: Schreiben Sie die Tests, führen Sie sie aus und sehen Sie, dass sie fehlschlagen. Schreiben Sie dann den minimalen Implementierungscode, damit sie bestanden werden. Sobald sie alle fertig sind, überarbeiten Sie sie bei Bedarf.
Die API
Die Dribbble-API ist ziemlich einfach. Zum jetzigen Zeitpunkt werden nur GET-Anforderungen unterstützt und es ist keine Authentifizierung erforderlich: ein idealer Kandidat für unser Lernprogramm. Darüber hinaus bietet es ein Limit von 60 Anrufen pro Minute, eine Einschränkung, die perfekt zeigt, warum die Arbeit mit APIs einen intelligenten Ansatz erfordert.
Schlüssel Konzepte
In diesem Tutorial muss davon ausgegangen werden, dass Sie mit den Testkonzepten vertraut sind: Vorrichtungen, Verspottungen, Erwartungen. Testen ist ein wichtiges Thema (insbesondere in der Ruby-Community). Auch wenn Sie kein Rubyist sind, möchte ich Sie ermutigen, sich eingehender mit der Angelegenheit zu befassen und nach gleichwertigen Tools für Ihre Alltagssprache zu suchen. Vielleicht möchten Sie das „RSpec-Buch“ von David Chelimsky et al. lesen, eine hervorragende Einführung in die verhaltensgesteuerte Entwicklung.
Um hier zusammenzufassen, sind hier drei Schlüsselkonzepte, die Sie kennen müssen:
- Mock: Auch Double genannt, ist ein Mock „ein Objekt, das in einem Beispiel für ein anderes Objekt steht“. Das heißt, wenn wir die Interaktion zwischen einem Objekt und einem anderen testen wollen, können wir das zweite verspotten. In diesem Tutorial werden wir die Dribbble-API verspotten. Um unseren Code zu testen, benötigen wir nicht die API selbst, sondern etwas, das sich so verhält und dieselbe Schnittstelle verfügbar macht.
- Fixture: Ein Datensatz, der einen bestimmten Status im System neu erstellt. Ein Fixture kann verwendet werden, um die erforderlichen Daten zum Testen einer Logik zu erstellen.
- Erwartung: Ein Testbeispiel, das aus der Sicht des Ergebnisses geschrieben wurde, das wir erreichen wollen.
Unsere Werkzeuge
"Führen Sie in der Regel jedes Mal Tests durch, wenn Sie sie aktualisieren."
WebMock ist eine Ruby-Verspottungsbibliothek, die zum Verspotten (oder Stubben) von http-Anforderungen verwendet wird. Mit anderen Worten, Sie können damit jede HTTP-Anforderung simulieren, ohne tatsächlich eine zu erstellen. Der Hauptvorteil dabei besteht darin, dass jeder HTTP-Dienst entwickelt und getestet werden kann, ohne dass der Dienst selbst benötigt wird und ohne dass damit verbundene Probleme (wie API-Beschränkungen, IP-Einschränkungen usw.) auftreten.VCR ist ein ergänzendes Tool, das jede echte http-Anfrage aufzeichnet und ein Fixture erstellt, eine Datei, die alle erforderlichen Daten enthält, um diese Anfrage zu replizieren, ohne sie erneut auszuführen. Wir werden es so konfigurieren, dass WebMock verwendet wird, um dies zu tun. Mit anderen Worten, unsere Tests werden nur einmal mit der echten Dribbble-API interagieren: Danach stummelt WebMock dank der vom Videorecorder aufgezeichneten Daten alle Anforderungen. Wir werden eine perfekte Nachbildung der lokal aufgezeichneten Dribbble-API-Antworten haben. Darüber hinaus können wir mit WebMock Edge-Fälle (wie das Zeitlimit für Anforderungen) einfach und konsistent testen. Eine wunderbare Folge unseres Setups ist, dass alles extrem schnell sein wird.
Für Unit-Tests verwenden wir Minitest. Es handelt sich um eine schnelle und einfache Unit-Testing-Bibliothek, die auch die Erwartungen in RSpec-Manier unterstützt. Es bietet einen kleineren Funktionsumfang, aber ich finde, dass dies Sie tatsächlich ermutigt und dazu drängt, Ihre Logik in kleine, testbare Methoden zu unterteilen. Minitest ist Teil von Ruby 1.9. Wenn Sie es also verwenden (ich hoffe es), müssen Sie es nicht installieren. Bei Ruby 1.8 ist es nur eine Frage von gem install minitest
.
Ich werde Ruby 1.9.3 verwenden: Wenn Sie dies nicht tun, werden Sie wahrscheinlich auf einige Probleme im Zusammenhang mit require_relative
stoßen, aber ich habe Fallback-Code in einen Kommentar direkt darunter eingefügt. Im Allgemeinen sollten Sie Tests jedes Mal ausführen, wenn Sie sie aktualisieren, auch wenn ich diesen Schritt im gesamten Tutorial nicht explizit erwähne.
Installieren



Wir werden die konventionelle Ordnerstruktur /lib
und /spec
verwenden, um unseren Code zu organisieren. Der Name unserer Bibliothek wird nach der Dribbble-Konvention zur Verwendung von Begriffen im Zusammenhang mit Basketball als Dish bezeichnet.
Das Gemfile enthält alle unsere Abhängigkeiten, auch wenn sie recht klein sind.
1 |
source :rubygems |
2 |
|
3 |
gem 'httparty' |
4 |
|
5 |
group :test do |
6 |
gem 'webmock' |
7 |
gem 'vcr' |
8 |
gem 'turn' |
9 |
gem 'rake' |
10 |
end
|
Httparty ist ein einfach zu verwendendes Juwel, um HTTP-Anfragen zu bearbeiten. Es wird der Kern unserer Bibliothek sein. In der Testgruppe werden wir auch Turn hinzufügen, um die Ausgabe unserer Tests so zu ändern, dass sie aussagekräftiger ist und Farbe unterstützt.
Die Ordner /lib
und /spec
haben eine symmetrische Struktur: Für jede Datei im Ordner /lib/dish
sollte sich eine Datei in /spec/dish
mit demselben Namen und dem Suffix "_spec" befinden.
Beginnen wir mit der Erstellung einer Datei /lib/dish.rb
und fügen den folgenden Code hinzu:
1 |
require "httparty" |
2 |
Dir[File.dirname(__FILE__) + '/dish/*.rb'].each do |file| |
3 |
require file |
4 |
end
|
Es macht nicht viel: Es erfordert "httparty" und iteriert dann über jede .rb
-Datei in /lib/dish
, um es zu benötigen. Mit dieser Datei können wir alle Funktionen in separaten Dateien in /lib/dish
hinzufügen und automatisch laden, indem wir nur diese einzelne Datei benötigen.
Wechseln wir in den Ordner /spec
. Hier ist der Inhalt der Datei spec_helper.rb
.
1 |
#we need the actual library file
|
2 |
require_relative '../lib/dish' |
3 |
# For Ruby < 1.9.3, use this instead of require_relative
|
4 |
# require(File.expand_path('../../lib/dish', __FILE__))
|
5 |
|
6 |
#dependencies
|
7 |
require 'minitest/autorun' |
8 |
require 'webmock/minitest' |
9 |
require 'vcr' |
10 |
require 'turn' |
11 |
|
12 |
Turn.config do |c| |
13 |
# :outline - turn's original case/test outline mode [default]
|
14 |
c.format = :outline |
15 |
# turn on invoke/execute tracing, enable full backtrace
|
16 |
c.trace = true |
17 |
# use humanized test names (works only with :outline format)
|
18 |
c.natural = true |
19 |
end
|
20 |
|
21 |
#VCR config
|
22 |
VCR.config do |c| |
23 |
c.cassette_library_dir = 'spec/fixtures/dish_cassettes' |
24 |
c.stub_with :webmock |
25 |
end
|
Hier gibt es einige bemerkenswerte Dinge, also lasst es uns Stück für Stück brechen:
- Zunächst benötigen wir die Hauptbibliotheksdatei für unsere App, damit der zu testende Code der Testsuite zur Verfügung steht. Die Anweisung
require_relative
ist eine Ergänzung zu Ruby 1.9.3. - Wir benötigen dann alle Bibliotheksabhängigkeiten:
minitest/autorun
enthält alle Erwartungen, die wir verwenden werden,webmock/minitest
fügt die erforderlichen Bindungen zwischen den beiden Bibliotheken hinzu, währendvcr
undturn
ziemlich selbsterklärend sind. - Der Turn-Konfigurationsblock muss lediglich unsere Testausgabe optimieren. Wir werden das Gliederungsformat verwenden, in dem wir die Beschreibung unserer Spezifikationen sehen können.
- Die VCR-Konfigurationsblöcke weisen VCR an, die Anforderungen in einem Fixture-Ordner zu speichern (beachten Sie den relativen Pfad) und WebMock als Stubbing-Bibliothek zu verwenden (VCR unterstützt einige andere).
Zu guter Letzt das Rakefile
, das Support-Code enthält:
1 |
require 'rake/testtask' |
2 |
|
3 |
Rake::TestTask.new do |t| |
4 |
t.test_files = FileList['spec/lib/dish/*_spec.rb'] |
5 |
t.verbose = true |
6 |
end
|
7 |
|
8 |
task :default => :test |
Die rake/testtask
-Bibliothek enthält eine TestTask
-Klasse, mit der Sie den Speicherort unserer Testdateien festlegen können. Von nun an geben wir zum Ausführen unserer Spezifikationen nur noch rake
aus dem Stammverzeichnis der Bibliothek ein.
Um unsere Konfiguration zu testen, fügen wir /lib/dish/player.rb
den folgenden Code hinzu:
1 |
module Dish |
2 |
class Player |
3 |
end
|
4 |
end
|
Dann /spec/lib/dish/player_spec.rb
:
1 |
require_relative '../../spec_helper' |
2 |
# For Ruby < 1.9.3, use this instead of require_relative
|
3 |
# require (File.expand_path('./../../../spec_helper', __FILE__))
|
4 |
|
5 |
describe Dish::Player do |
6 |
|
7 |
it "must work" do |
8 |
"Yay!".must_be_instance_of String |
9 |
end
|
10 |
|
11 |
end
|
Wenn Sie rake
ausführen, sollten Sie einen Test bestehen und keine Fehler machen. Dieser Test ist für unser Projekt keineswegs nützlich, überprüft jedoch implizit, ob unsere Bibliotheksdateistruktur vorhanden ist (der describe
-Block würde einen Fehler auslösen, wenn das Dish::Player
-Modul nicht geladen würde).
Erste Spezifikationen
Um richtig zu funktionieren, benötigt Dish die Httparty-Module und die richtige base_uri
, d. h. Die Basis-URL der Dribbble-API. Schreiben wir die relevanten Tests für diese Anforderungen in player_spec.rb
:
1 |
...
|
2 |
describe Dish::Player do |
3 |
|
4 |
describe "default attributes" do |
5 |
|
6 |
it "must include httparty methods" do |
7 |
Dish::Player.must_include HTTParty |
8 |
end
|
9 |
|
10 |
it "must have the base url set to the Dribble API endpoint" do |
11 |
Dish::Player.base_uri.must_equal 'http://api.dribbble.com' |
12 |
end
|
13 |
|
14 |
end
|
15 |
|
16 |
end
|
Wie Sie sehen können, sind die Erwartungen von Minitest selbsterklärend, insbesondere wenn Sie ein RSpec-Benutzer sind: Der größte Unterschied besteht in der Formulierung, bei der Minitest "must/wont" gegenüber "should/should_not" bevorzugt.
Wenn Sie diese Tests ausführen, werden ein Fehler und ein Fehler angezeigt. Um sie zu bestehen, fügen wir player.rb
unsere ersten Zeilen des Implementierungscodes hinzu:
1 |
module Dish |
2 |
|
3 |
class Player |
4 |
|
5 |
include HTTParty |
6 |
|
7 |
base_uri 'http://api.dribbble.com' |
8 |
|
9 |
end
|
10 |
|
11 |
end
|
Wenn Sie den rake
erneut ausführen, sollten die beiden Spezifikationen angezeigt werden. Jetzt hat unsere Player
-Klasse Zugriff auf alle Httparty-Klassenmethoden wie get
oder post
.
Aufzeichnung unserer ersten Anfrage
Da wir an der Player
-Klasse arbeiten, benötigen wir API-Daten für einen Player. Die Dokumentationsseite der Dribbble-API zeigt, dass der Endpunkt zum Abrufen von Daten zu einem bestimmten Player http://api.dribbble.com/players/:id
ist
Wie in der typischen Rails-Mode :id
ist entweder die id oder der Benutzername eines bestimmten Spielers. Wir werden simplebits
verwenden, den Benutzernamen von Dan Cederholm, einem der Dribbble-Gründer.
Um die Anforderung mit dem Videorecorder aufzuzeichnen, aktualisieren wir unsere Datei player_spec.rb
, indem wir der Spezifikation direkt nach dem ersten den folgenden describe
-Block hinzufügen:
1 |
...
|
2 |
|
3 |
describe "GET profile" do |
4 |
|
5 |
before do |
6 |
VCR.insert_cassette 'player', :record => :new_episodes |
7 |
end
|
8 |
|
9 |
after do |
10 |
VCR.eject_cassette |
11 |
end
|
12 |
|
13 |
it "records the fixture" do |
14 |
Dish::Player.get('/players/simplebits') |
15 |
end
|
16 |
|
17 |
end
|
18 |
|
19 |
end
|
Nach dem Ausführen von
rake
können Sie überprüfen, ob das Gerät erstellt wurde. Von nun an sind alle unsere Tests vollständig netzwerkunabhängig.
Der before
-Block wird verwendet, um einen bestimmten Teil des Codes vor jeder Erwartung auszuführen: Wir verwenden ihn, um das VCR-Makro hinzuzufügen, mit dem ein Gerät aufgezeichnet wird, das wir als "Player" bezeichnen. Dadurch wird eine player.yml
-Datei unter spec/fixtures/dish_cassettes
erstellt. Mit der Option :record
werden alle neuen Anforderungen einmal aufgezeichnet und bei jeder nachfolgenden identischen Anforderung erneut abgespielt. Als Proof of Concept können wir eine Spezifikation hinzufügen, deren einziges Ziel darin besteht, ein Gerät für das Profil von simplebits aufzuzeichnen. Die after
-Direktive weist den Videorecorder an, die Kassette nach den Tests zu entfernen, um sicherzustellen, dass alles ordnungsgemäß isoliert ist. Die get
-Methode für die Player
-Klasse wird dank des Httparty
-Moduls zur Verfügung gestellt.
Nach dem Ausführen von rake
können Sie überprüfen, ob das Gerät erstellt wurde. Von nun an sind alle unsere Tests vollständig netzwerkunabhängig.
Abrufen des Spielerprofils



Jeder Dribbble-Benutzer hat ein Profil, das eine ziemlich umfangreiche Datenmenge enthält. Lassen Sie uns darüber nachdenken, wie unsere Bibliothek bei tatsächlicher Nutzung aussehen soll: Dies ist eine nützliche Methode, um unser DSL zu verfeinern. Folgendes möchten wir erreichen:
1 |
simplebits = Dish::Player.new('simplebits') |
2 |
simplebits.profile |
3 |
=> #returns a hash with all the data from the API |
4 |
simplebits.username |
5 |
=> 'simplebits' |
6 |
simplebits.id |
7 |
=> 1 |
8 |
simplebits.shots_count |
9 |
=> 157 |
Einfach und effektiv: Wir möchten einen Player mithilfe seines Benutzernamens instanziieren und dann auf seine Daten zugreifen, indem wir Methoden für die Instanz aufrufen, die den von der API zurückgegebenen Attributen zugeordnet sind. Wir müssen mit der API selbst konsistent sein.
Lassen Sie uns jeweils eine Sache angehen und einige Tests schreiben, die sich auf das Abrufen der Spielerdaten von der API beziehen. Wir können unseren Block "GET-profile"
so ändern, dass er Folgendes enthält:
1 |
describe "GET profile" do |
2 |
|
3 |
let(:player) { Dish::Player.new } |
4 |
|
5 |
before do |
6 |
VCR.insert_cassette 'player', :record => :new_episodes |
7 |
end
|
8 |
|
9 |
after do |
10 |
VCR.eject_cassette |
11 |
end
|
12 |
|
13 |
it "must have a profile method" do |
14 |
player.must_respond_to :profile |
15 |
end
|
16 |
|
17 |
it "must parse the api response from JSON to Hash" do |
18 |
player.profile.must_be_instance_of Hash |
19 |
end
|
20 |
|
21 |
it "must perform the request and get the data" do |
22 |
player.profile["username"].must_equal 'simplebits' |
23 |
end
|
24 |
|
25 |
end
|
Die let
-Direktive oben erstellt eine Dish::Player
-Instanz, die in den Erwartungen verfügbar ist. Als Nächstes möchten wir sicherstellen, dass unser Player über eine Profilmethode verfügt, deren Wert ein Hash ist, der die Daten aus der API darstellt. Als letzten Schritt testen wir einen Beispielschlüssel (den Benutzernamen), um sicherzustellen, dass wir die Anforderung tatsächlich ausführen.
Beachten Sie, dass wir uns noch nicht mit dem Festlegen des Benutzernamens befassen, da dies ein weiterer Schritt ist. Die minimal erforderliche Implementierung ist die folgende:
1 |
...
|
2 |
class Player |
3 |
|
4 |
include HTTParty |
5 |
|
6 |
base_uri 'http://api.dribbble.com' |
7 |
|
8 |
def profile |
9 |
self.class.get '/players/simplebits' |
10 |
end
|
11 |
|
12 |
end
|
13 |
...
|
Eine sehr kleine Menge Code: Wir verpacken nur einen get-Aufruf in die profile
-Methode. Wir übergeben dann den fest codierten Pfad, um die Daten von simplebits abzurufen, Daten, die wir dank VCR bereits gespeichert hatten.
Alle unsere Tests sollten bestanden werden.
Benutzernamen einstellen
Nachdem wir nun eine funktionierende Profilfunktion haben, können wir uns um den Benutzernamen kümmern. Hier sind die relevanten Spezifikationen:
1 |
describe "default instance attributes" do |
2 |
|
3 |
let(:player) { Dish::Player.new('simplebits') } |
4 |
|
5 |
it "must have an id attribute" do |
6 |
player.must_respond_to :username |
7 |
end
|
8 |
|
9 |
it "must have the right id" do |
10 |
player.username.must_equal 'simplebits' |
11 |
end
|
12 |
|
13 |
end
|
14 |
|
15 |
describe "GET profile" do |
16 |
|
17 |
let(:player) { Dish::Player.new('simplebits') } |
18 |
|
19 |
before do |
20 |
VCR.insert_cassette 'base', :record => :new_episodes |
21 |
end
|
22 |
|
23 |
after do |
24 |
VCR.eject_cassette |
25 |
end
|
26 |
|
27 |
it "must have a profile method" do |
28 |
player.must_respond_to :profile |
29 |
end
|
30 |
|
31 |
it "must parse the api response from JSON to Hash" do |
32 |
player.profile.must_be_instance_of Hash |
33 |
end
|
34 |
|
35 |
it "must get the right profile" do |
36 |
player.profile["username"].must_equal "simplebits" |
37 |
end
|
38 |
|
39 |
end
|
Wir haben einen neuen Beschreibungsblock hinzugefügt, um den Benutzernamen zu überprüfen, den wir hinzufügen möchten, und einfach die player
-Initialisierung im GET profile
-Block geändert, um die gewünschte DSL widerzuspiegeln. Wenn Sie die Spezifikationen jetzt ausführen, werden viele Fehler angezeigt, da unsere Player
-Klasse bei der Initialisierung(vorerst) keine Argumente akzeptiert.
Die Implementierung ist sehr einfach:
1 |
...
|
2 |
class Player |
3 |
|
4 |
attr_accessor :username |
5 |
|
6 |
include HTTParty |
7 |
|
8 |
base_uri 'http://api.dribbble.com' |
9 |
|
10 |
def initialize(username) |
11 |
self.username = username |
12 |
end
|
13 |
|
14 |
def profile |
15 |
self.class.get "/players/#{self.username}" |
16 |
end
|
17 |
|
18 |
end
|
19 |
...
|
Die initialize-Methode erhält einen Benutzernamen, der dank der oben hinzugefügten attr_accessor
-Methode in der Klasse gespeichert wird. Wir ändern dann die Profilmethode, um das Benutzername-Attribut zu interpolieren.
Wir sollten alle unsere Tests noch einmal bestehen lassen.
Dynamische Attribute
Grundsätzlich ist unsere Bibliothek in einem ziemlich guten Zustand. Da das Profil ein Hash ist, können wir hier anhalten und es bereits verwenden, indem wir den Schlüssel des Attributs übergeben, für das wir den Wert erhalten möchten. Unser Ziel ist es jedoch, ein einfach zu verwendendes DSL zu erstellen, das für jedes Attribut eine Methode enthält.
Überlegen wir uns, was wir erreichen müssen. Nehmen wir an, wir haben eine Player-Instanz und geben an, wie das funktionieren würde:
1 |
player.username |
2 |
=> 'simplebits' |
3 |
player.shots_count |
4 |
=> 157 |
5 |
player.foo_attribute |
6 |
=> NoMethodError |
Lassen Sie uns dies in Spezifikationen übersetzen und sie dem GET profile
-Block hinzufügen:
1 |
...
|
2 |
describe "dynamic attributes" do |
3 |
|
4 |
before do |
5 |
player.profile |
6 |
end
|
7 |
|
8 |
it "must return the attribute value if present in profile" do |
9 |
player.id.must_equal 1 |
10 |
end
|
11 |
|
12 |
it "must raise method missing if attribute is not present" do |
13 |
lambda { player.foo_attribute }.must_raise NoMethodError |
14 |
end
|
15 |
|
16 |
end
|
17 |
...
|
Wir haben bereits eine Spezifikation für den Benutzernamen, sodass wir keine weitere hinzufügen müssen. Beachten Sie einige Dinge:
- Wir rufen
player.profile
explizit in einem Vorher-Block auf, andernfalls ist es Null, wenn wir versuchen, den Attributwert abzurufen. - Um zu testen, ob
foo_attribute
eine Ausnahme auslöst, müssen wir es in ein Lambda einschließen und überprüfen, ob es den erwarteten Fehler auslöst. - Wir testen, dass
id
gleich1
ist, da wir wissen, dass dies der erwartete Wert ist (dies ist ein rein datenabhängiger Test).
In Bezug auf die Implementierung könnten wir eine Reihe von Methoden definieren, um auf den profil
-Hash zuzugreifen. Dies würde jedoch eine Menge doppelter Logik erzeugen. Darüber hinaus würde sich das auf das API-Ergebnis verlassen, um immer die gleichen Schlüssel zu haben.
"Wir werden uns auf
method_missing
verlassen, um diese Fälle zu behandeln und all diese Methoden im laufenden Betrieb zu generieren."
Stattdessen werden wir uns auf method_missing
verlassen, um diese Fälle zu behandeln und alle diese Methoden im laufenden Betrieb zu "generieren". Aber was bedeutet das? Ohne zu viel Metaprogrammierung zu betreiben, können wir einfach sagen, dass Ruby jedes Mal, wenn wir eine Methode aufrufen, die nicht für das Objekt vorhanden ist, einen NoMethodError
mithilfe von method_missing
auslöst. Indem wir genau diese Methode innerhalb einer Klasse neu definieren, können wir ihr Verhalten ändern.
In unserem Fall werden wir den Aufruf method_missing
abfangen, überprüfen, ob der aufgerufene Methodenname ein Schlüssel im Profil-Hash ist, und im Falle eines positiven Ergebnisses den Hash-Wert für diesen Schlüssel zurückgeben. Wenn nicht, rufen wir super
auf, um einen Standard-NoMethodError
auszulösen: Dies ist erforderlich, um sicherzustellen, dass sich unsere Bibliothek genau so verhält, wie es jede andere Bibliothek tun würde. Mit anderen Worten, wir möchten die geringstmögliche Überraschung garantieren.
Fügen wir der Player
-Klasse den folgenden Code hinzu:
1 |
def method_missing(name, *args, &block) |
2 |
if profile.has_key?(name.to_s) |
3 |
profile[name.to_s] |
4 |
else
|
5 |
super
|
6 |
end
|
7 |
end
|
Der Code macht genau das, was oben beschrieben wurde. Wenn Sie jetzt die Spezifikationen ausführen, sollten Sie sie alle bestehen lassen. Ich möchte Sie bitten, den Spezifikationsdateien weitere Elemente für ein anderes Attribut hinzuzufügen, z. B. shot_count
.
Diese Implementierung ist jedoch nicht wirklich idiomatisch Ruby. Es funktioniert, kann aber zu einem ternären Operator optimiert werden, einer komprimierten Form einer If-else-Bedingung. Es kann wie folgt umgeschrieben werden:
1 |
def method_missing(name, *args, &block) |
2 |
profile.has_key?(name.to_s) ? profile[name.to_s] : super |
3 |
end
|
Es geht nicht nur um die Länge, sondern auch um Konsistenz und gemeinsame Konventionen zwischen Entwicklern. Das Durchsuchen des Quellcodes von Ruby-Edelsteinen und -Bibliotheken ist eine gute Möglichkeit, sich an diese Konventionen zu gewöhnen.
Caching
Als letzten Schritt möchten wir sicherstellen, dass unsere Bibliothek effizient ist. Es sollte nicht mehr Anfragen als nötig stellen und möglicherweise Daten intern zwischenspeichern. Lassen Sie uns noch einmal darüber nachdenken, wie wir es verwenden könnten:
1 |
player.profile |
2 |
=> performs the request and returns a Hash |
3 |
player.profile |
4 |
=> returns the same hash |
5 |
player.profile(true) |
6 |
=> forces the reload of the http request and then returns the hash (with data changes if necessary) |
Wie können wir das testen? Wir können WebMock verwenden, um Netzwerkverbindungen zum API-Endpunkt zu aktivieren und zu deaktivieren. Selbst wenn wir Videorecorder-Geräte verwenden, kann WebMock ein Netzwerk-Timeout oder eine andere Reaktion auf den Server simulieren. In unserem Fall können wir das Caching testen, indem wir das Profil einmal abrufen und dann das Netzwerk deaktivieren. Durch erneutes Aufrufen von player.profile
sollten dieselben Daten angezeigt werden, während durch Aufrufen von player.profile(true)
ein Timeout::Error
angezeigt wird, da die Bibliothek versuchen würde, eine Verbindung zum (deaktivierten) API-Endpunkt herzustellen.
Fügen wir der Datei player_spec.rb
direkt nach dynamic attribute generation
einen weiteren Block hinzu:
1 |
describe "caching" do |
2 |
|
3 |
# we use Webmock to disable the network connection after
|
4 |
# fetching the profile
|
5 |
before do |
6 |
player.profile |
7 |
stub_request(:any, /api.dribbble.com/).to_timeout |
8 |
end
|
9 |
|
10 |
it "must cache the profile" do |
11 |
player.profile.must_be_instance_of Hash |
12 |
end
|
13 |
|
14 |
it "must refresh the profile if forced" do |
15 |
lambda { player.profile(true) }.must_raise Timeout::Error |
16 |
end
|
17 |
|
18 |
end
|
Die Methode stub_request
fängt alle Aufrufe des API-Endpunkts ab und simuliert ein Timeout, wodurch der erwartete Timeout::Error
ausgelöst wird. Wie zuvor testen wir das Vorhandensein dieses Fehlers in einem Lambda.
Die Implementierung kann schwierig sein, daher teilen wir sie in zwei Schritte auf. Verschieben wir zunächst die eigentliche http-Anforderung in eine private Methode:
1 |
...
|
2 |
def profile |
3 |
get_profile
|
4 |
end
|
5 |
|
6 |
...
|
7 |
|
8 |
private
|
9 |
|
10 |
def get_profile |
11 |
self.class.get("/players/#{self.username}") |
12 |
end
|
13 |
...
|
Dadurch werden unsere Spezifikationen nicht weitergegeben, da das Ergebnis von get_profile
nicht zwischengespeichert wird. Ändern Sie dazu die profile
-Methode:
1 |
...
|
2 |
def profile |
3 |
@profile ||= get_profile |
4 |
end
|
5 |
...
|
Wir werden den Ergebnis-Hash in einer Instanzvariablen speichern. Beachten Sie auch den Operator ||=
, dessen Vorhandensein sicherstellt, dass get_profile
nur ausgeführt wird, wenn @profile einen falschen Wert zurückgibt (wie nil
).
Als nächstes können wir die Direktive zum erzwungenen Neuladen hinzufügen:
1 |
...
|
2 |
def profile(force = false) |
3 |
force ? @profile = get_profile : @profile ||= get_profile |
4 |
end
|
5 |
...
|
Wir verwenden wieder ein Ternär: Wenn force
falsch ist, führen wir get_profile
aus und zwischenspeichern es. Wenn nicht, verwenden wir die in der vorherigen Version dieser Methode geschriebene Logik (dh führen die Anforderung nur aus, wenn wir noch keinen Hash haben ).
Unsere Spezifikationen sollten jetzt grün sein und dies ist auch das Ende unseres Tutorials.
Einpacken
Unser Ziel in diesem Tutorial war es, eine kleine und effiziente Bibliothek für die Interaktion mit der Dribbble-API zu schreiben. Wir haben den Grundstein dafür gelegt. Der größte Teil der von uns geschriebenen Logik kann abstrahiert und für den Zugriff auf alle anderen Endpunkte wiederverwendet werden. Minitest, WebMock und VCR haben sich als wertvolle Werkzeuge erwiesen, mit denen wir unseren Code gestalten können.
Wir müssen uns jedoch einer kleinen Einschränkung bewusst sein: Der Videorecorder kann zu einem zweischneidigen Schwert werden, da unsere Tests zu stark datenabhängig werden können. Wenn die API, die wir erstellen, aus irgendeinem Grund gegen Änderungen ohne sichtbares Zeichen (z. B. eine Versionsnummer) erstellt wird, besteht die Gefahr, dass unsere Tests perfekt mit einem Datensatz funktionieren, der nicht mehr relevant ist. In diesem Fall ist das Entfernen und Neuerstellen des Geräts der beste Weg, um sicherzustellen, dass unser Code weiterhin wie erwartet funktioniert.