Advertisement
  1. Code
  2. Ruby

Schreiben eines API-Wrappers in Ruby mit TDD

Scroll to top
Read Time: 19 min

() 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

SetupSetupSetup

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ährend vcr und turn 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

DribbbleDribbbleDribbble

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 gleich 1 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.