Écrire un wrapper d'API en Ruby avec TDD

Tôt ou tard, tous les développeurs doivent interagir avec une API. La partie la plus difficile est toujours de tester de manière fiable le code que nous écrivons et, comme nous voulons nous assurer que tout fonctionne correctement, nous exécutons en permanence du code qui interroge l'API elle-même. Ce processus est lent et inefficace, car nous pouvons rencontrer des problèmes de réseau et des incohérences dans les données (les résultats de l'API peuvent changer). Passons en revue comment nous pouvons éviter tout cet effort avec Ruby.


Notre objectif

"Le flux est essentiel: écrivez les tests, exécutez-les et voyez-les échouer, puis écrivez le code d'implémentation minimal pour les faire passer. Une fois qu'ils ont tous terminé, effectuez une refactorisation si nécessaire."

Notre objectif est simple: rédigez un petit wrapper autour de l'API Dribbble pour récupérer des informations sur un utilisateur (appelé "lecteur" dans le monde de Dribbble)..
Comme nous allons utiliser Ruby, nous suivrons également une approche TDD: si vous n'êtes pas familier avec cette technique, Nettuts + a un bon aperçu de RSpec que vous pouvez lire. En un mot, nous allons rédiger des tests avant de rédiger notre implémentation de code, ce qui facilitera la détection des bogues et l’obtention d’une qualité de code élevée. Le flux est essentiel: écrivez les tests, exécutez-les et voyez-les échouer, puis écrivez le code d'implémentation minimal pour les faire passer. Une fois qu'ils ont tous fait, refactor si nécessaire.

L'API

L'API Dribbble est assez simple. À l'heure actuelle, il ne prend en charge que les requêtes GET et ne nécessite pas d'authentification: un candidat idéal pour notre tutoriel. De plus, il offre une limite de 60 appels par minute, une restriction qui montre parfaitement pourquoi l'utilisation d'API nécessite une approche intelligente..


Concepts clés

Ce tutoriel doit supposer que vous maîtrisez les concepts de test: installations, simulacres, attentes. Le test est un sujet important (en particulier dans la communauté Ruby) et même si vous n'êtes pas un rubyiste, je vous encourage à approfondir la question et à rechercher des outils équivalents pour votre langage de tous les jours. Vous voudrez peut-être lire «Le livre RSpec» de David Chelimsky et al., Une excellente introduction au développement fondé sur le comportement.

Pour résumer ici, voici trois concepts clés que vous devez connaître:

  • Moquer: également appelé double, une maquette est «un objet qui remplace un autre objet dans un exemple». Cela signifie que si nous voulons tester l’interaction entre un objet et un autre, nous pouvons nous moquer du second. Dans ce tutoriel, nous allons nous moquer de l'API Dribbble, car pour tester notre code, nous n'avons pas besoin de l'API elle-même, mais quelque chose qui se comporte de la sorte et qui expose la même interface..
  • Fixation: un jeu de données qui recrée un état spécifique dans le système. Un appareil peut être utilisé pour créer les données nécessaires pour tester une logique.
  • Attente: un exemple de test écrit du point de vue du résultat que nous voulons atteindre.

Nos outils

"En règle générale, effectuez des tests chaque fois que vous les mettez à jour."

WebMock est une bibliothèque Ruby mocking utilisée pour simuler (ou stub) des requêtes http. En d'autres termes, cela vous permet de simuler n'importe quelle requête HTTP sans en faire une. Le principal avantage de cela est de pouvoir développer et tester tout service HTTP sans avoir besoin du service lui-même et sans encourir de problèmes connexes (comme les limites d'API, les restrictions IP, etc.)..
VCR est un outil complémentaire qui enregistre toute demande http réelle et crée un appareil, un fichier contenant toutes les données nécessaires pour répliquer cette demande sans l'exécuter à nouveau. Nous allons le configurer pour utiliser WebMock. En d'autres termes, nos tests n'interagiront qu'une seule fois avec la véritable API Dribbble: après cela, WebMock stub toutes les requêtes grâce aux données enregistrées par le magnétoscope. Nous aurons une réplique parfaite des réponses de l'API Dribbble enregistrées localement. De plus, WebMock nous permettra de tester facilement et systématiquement les cas critiques (comme le délai de traitement des demandes). Une conséquence merveilleuse de notre configuration est que tout sera extrêmement rapide.

En ce qui concerne les tests unitaires, nous utiliserons Minitest. C'est une bibliothèque de tests unitaires simple et rapide qui prend également en charge les attentes de la manière RSpec. Il propose un ensemble de fonctionnalités plus réduit, mais j’aperçois que cela vous encourage et vous pousse à séparer votre logique en petites méthodes testables. Minitest fait partie de Ruby 1.9, donc si vous l'utilisez (j'espère), vous n'avez pas besoin de l'installer. Sur Ruby 1.8, ce n’est qu’une question de bijou installer minitest.

J'utiliserai Ruby 1.9.3: sinon, vous rencontrerez probablement des problèmes liés à require_relative, mais j'ai inclus le code de secours dans un commentaire juste en dessous. En règle générale, vous devez exécuter des tests chaque fois que vous les mettez à jour, même si je ne mentionnerai pas explicitement cette étape tout au long du didacticiel..


Installer

Nous allons utiliser le conventionnel / lib et / spec structure de dossier pour organiser notre code. Quant au nom de notre bibliothèque, nous l'appellerons Plat, suivant la convention Dribbble d'utilisation de termes liés au basketball.

Le Gemfile contiendra toutes nos dépendances, bien qu'elles soient assez petites.

 source: groupe de rubygems 'httparty': test de gem 'webmock' gem 'vcr' gem 'tourner' gem 'rake' end

Httparty est un joyau facile à utiliser pour gérer les requêtes HTTP. ce sera le noyau de notre bibliothèque. Dans le groupe de tests, nous ajouterons également Turn pour modifier le résultat de nos tests afin de le rendre plus descriptif et de prendre en charge les couleurs..

le / lib et / spec les dossiers ont une structure symétrique: pour chaque fichier contenu dans le / lib / dish dossier, il devrait y avoir un fichier à l'intérieur / spec / plat avec le même nom et le suffixe '_spec'.

Commençons par créer un /lib/dish.rb fichier et ajoutez le code suivant:

 require "httparty" Dir [File.dirname (__ FILE__) + '/dish/*.rb'aser.each do | fichier | nécessite la fin du fichier

Cela ne fait pas grand chose: il faut «httparty», puis itère chaque fois .rb déposer à l'intérieur / lib / dish l'exiger. Avec ce fichier en place, nous pourrons ajouter toute fonctionnalité dans des fichiers séparés de / lib / dish et le faire charger automatiquement juste en exigeant ce fichier unique.

Passons au / spec dossier. Voici le contenu de la spec_helper.rb fichier.

 #nous avons besoin du fichier de bibliothèque réel require_relative '… / lib / dish' # pour 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

Il y a pas mal de choses à noter ici, alors cassons-les pièce par pièce:

  • Au début, nous avons besoin du fichier lib principal pour notre application, ce qui rend le code que nous voulons tester disponible pour la suite de tests. le require_relative déclaration est un ajout Ruby 1.9.3.
  • Nous avons ensuite besoin de toutes les dépendances de la bibliothèque: minitest / autorun comprend toutes les attentes que nous allons utiliser, webmock / minitest ajoute les liaisons nécessaires entre les deux bibliothèques, tandis que vcr et tour sont assez explicites.
  • Le bloc de configuration Turn doit simplement modifier notre sortie de test. Nous allons utiliser le format de contour, où nous pouvons voir la description de nos spécifications.
  • Les blocs de configuration du magnétoscope indiquent au magnétoscope de stocker les demandes dans un dossier d’appareils (notez le chemin relatif) et d’utiliser WebMock comme bibliothèque de remplacement (le magnétoscope en prend en charge d’autres)..

Dernier point, mais non le moindre, le Rakefile qui contient du code de support:

 nécessite 'rake / testtask' Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / plat / * _ spec.rb'] t.verbose = true fin de tâche: default =>: test

le rake / testtask bibliothèque comprend un TestTask classe utile pour définir l’emplacement de nos fichiers de test. A partir de maintenant, pour exécuter nos spécifications, nous ne taperons que râteau à partir du répertoire racine de la bibliothèque.

Pour tester notre configuration, ajoutons le code suivant à /lib/dish/player.rb:

 module Dish class Player end end

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

 require_relative '… /… / spec_helper' # Pour 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

Fonctionnement râteau devrait vous donner un test de réussite et aucune erreur. Ce test n’est nullement utile pour notre projet, mais il vérifie implicitement que la structure de fichier de notre bibliothèque est en place (le décrire bloquer lancerait une erreur si le Dish :: Joueur le module n'a pas été chargé).


Premières spécifications

Pour fonctionner correctement, Dish requiert les modules Httparty et le bon base_uri, c'est-à-dire l'URL de base de l'API Dribbble. Écrivons les tests pertinents pour ces exigences dans player_spec.rb:

… Décrire Dish :: Player décrit les "attributs par défaut" do it "doit inclure les méthodes httparty" do Dish :: Player.must_include HTTParty end it ", l'URL de base doit être définie sur le point de terminaison de l'API Dribble" do Dish :: Player.base_uri .must_equal 'http://api.dribbble.com' fin fin fin

Comme vous pouvez le constater, les attentes de Minitest s’expliquent d’elles-mêmes, en particulier si vous êtes un utilisateur de RSpec: la plus grande différence réside dans la formulation, Minitest préférant «doit / doit» à «devrait / ne devrait pas»..

L'exécution de ces tests affichera une erreur et un échec. Pour les faire passer, ajoutons nos premières lignes de code d'implémentation à player.rb:

 module Dish class Player inclut HTTParty base_uri 'http://api.dribbble.com' end end

Fonctionnement râteau encore une fois devrait montrer les deux spécifications en passant. Maintenant notre Joueur class a accès à toutes les méthodes de la classe Httparty, comme obtenir ou poster.


Enregistrement de notre première demande

Comme nous allons travailler sur le Joueur classe, nous aurons besoin de données API pour un joueur. La page de documentation de l'API Dribbble indique que le point de terminaison pour obtenir des données sur un lecteur spécifique est http://api.dribbble.com/players/:id

Comme dans la mode typique de Rails, : id est soit le identifiant ou la Nom d'utilisateur d'un joueur spécifique. Nous allons utiliser simples bits, le nom d'utilisateur de Dan Cederholm, l'un des fondateurs de Dribbble.

Pour enregistrer la demande avec le magnétoscope, mettons à jour notre player_spec.rb fichier en ajoutant ce qui suit décrire bloquer à la spécification, juste après le premier:

… Décrivent le «profil GET» avant VCR.insert_cassette 'player',: record =>: new_episodes se termine après VCR.eject_cassette end it "enregistre le dispositif" do Dish :: Player.get ('/ players / simplebits') fin fin fin

Après avoir couru râteau, vous pouvez vérifier que le projecteur a été créé. A partir de maintenant, tous nos tests seront totalement indépendants du réseau.

le avant block est utilisé pour exécuter une partie spécifique du code avant chaque attente: nous l'utilisons pour ajouter la macro de magnétoscope utilisée pour enregistrer un appareil que nous appellerons «lecteur». Cela va créer un player.yml déposer sous spec / fixtures / dish_cassettes. le :record L'option est configurée pour enregistrer toutes les nouvelles demandes une fois et les rejouer à chaque demande identique ultérieure. Comme preuve de concept, nous pouvons ajouter une spécification dont le seul but est d’enregistrer un montage pour le profil de simplebits. le après directive indique au magnétoscope de retirer la cassette après les tests, en s'assurant que tout est correctement isolé. le obtenir méthode sur le Joueur classe est mise à disposition, grâce à l'inclusion du Httparty module.

Après avoir couru râteau, vous pouvez vérifier que le projecteur a été créé. A partir de maintenant, tous nos tests seront totalement indépendants du réseau.


Obtenir le profil du joueur

Chaque utilisateur de Dribbble a un profil qui contient une quantité assez importante de données. Pensons à la façon dont nous aimerions que notre bibliothèque soit réellement utilisée: c’est un moyen utile d’étoffer notre DSL. Voici ce que nous voulons réaliser:

 simplebits = Dish :: Player.new ('simplebits') simplebits.profile => # renvoie un hachage avec toutes les données de l'API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157

Simple et efficace: nous voulons instancier un lecteur en utilisant son nom d'utilisateur, puis accéder à ses données en appelant des méthodes sur l'instance qui mappe les attributs renvoyés par l'API. Nous devons être cohérents avec l'API elle-même.

Abordons une chose à la fois et écrivons quelques tests liés à la récupération des données du joueur à partir de l'API. Nous pouvons modifier notre "Profil GET" bloquer pour avoir:

 Décrivez "profil GET" laissez let (: player) Dish :: Player.new avant VCR.insert_cassette 'player',: record =>: new_episodes end after do VCR.eject_cassette end it "doit avoir une méthode de profil" do player.must_respond_to: profile end it "doit analyser la réponse de l'API de JSON au hachage" do player.profile.must_be_instance_of Hash end it "doit exécuter la demande et obtenir les données" do player.profile ["nomutilisateur"]. must_equal 'simplebits 'fin fin

le laisser directive au sommet crée un Dish :: Joueur exemple disponible dans les attentes. Ensuite, nous voulons nous assurer que notre lecteur dispose d’une méthode de profil ayant pour valeur un hachage représentant les données de l’API. Dans une dernière étape, nous testons un exemple de clé (le nom d'utilisateur) pour nous assurer que la requête est bien exécutée..

Notez que nous ne savons pas encore comment définir le nom d'utilisateur, car il s'agit d'une étape ultérieure. L'implémentation minimale requise est la suivante:

… Class Player inclut HTTParty base_uri 'http://api.dribbble.com' def profile self.class.get '/ players / simplebits' end end… 

Une très petite quantité de code: nous venons d’envelopper un appel get dans le profil méthode. Nous passons ensuite le chemin codé en dur pour récupérer les données de simplebits, données que nous avions déjà stockées grâce au magnétoscope..

Tous nos tests devraient passer.


Définition du nom d'utilisateur

Maintenant que nous avons une fonction de profil opérationnelle, nous pouvons prendre en charge le nom d'utilisateur. Voici les spécifications pertinentes:

 décris les "attributs d'instance par défaut" do let (: player) Dish :: Player.new ('simplebits') it "doit avoir un attribut id" do player.must_respond_to: username end it "doit avoir le bon id" do player .username.must_equal fin 'simplebits' décrivent le "profil GET" laissez (: player) Dish :: Player.new ('simplebits') avant VCR.insert_cassette 'base',: record =>: new_episodes se termine après do VCR.eject_cassette end it "doit avoir une méthode de profil" do player.must_respond_to: profile end it "doit analyser la réponse de l'API de JSON à Hash" do player.profile.must_be_instance_of Hash end it "doit obtenir le bon profil" do player .profile ["nom d'utilisateur"]. must_equal "simplebits" end end

Nous avons ajouté un nouveau bloc de description pour vérifier le nom d'utilisateur que nous allons ajouter et simplement modifié le joueur initialisation dans le GET profil bloquer pour refléter le DSL que nous voulons avoir. Exécuter les spécifications maintenant révélera de nombreuses erreurs, comme notre Joueur la classe n'accepte pas les arguments à l'initialisation (pour l'instant).

La mise en œuvre est très simple:

… Class Player attr_accessor: le nom d'utilisateur est inclus avec HTTParty base_uri 'http://api.dribbble.com' def initialize (nom d'utilisateur) self.username = nom d'utilisateur end profile de self.class.get "/players/#self.username" fin… 

La méthode initialize obtient un nom d’utilisateur qui est stocké dans la classe grâce au attr_accessor méthode ajoutée ci-dessus. Nous changeons ensuite la méthode de profil pour interpoler l'attribut nom d'utilisateur.

Nous devrions faire passer tous nos tests une fois de plus.


Attributs Dynamiques

Au niveau de base, notre bibliothèque est en assez bonne forme. Comme le profil est un hachage, nous pourrions nous arrêter ici et l’utiliser déjà en transmettant la clé de l’attribut pour lequel nous voulons obtenir la valeur. Notre objectif, cependant, est de créer un DSL facile à utiliser avec une méthode pour chaque attribut..

Pensons à ce que nous devons réaliser. Supposons que nous ayons une instance de joueur et un talon comment cela fonctionnerait:

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

Traduisons cela en spécifications et ajoutez-les à la GET profil bloc:

… Décrire "les attributs dynamiques" faire avant que player.profile end it "doit renvoyer la valeur de l'attribut s'il est présent dans le profil" do player.id.must_equal 1 end it ", la méthode de relance manquante si l'attribut n'est pas présent" do lambda player. foo_attribute .must_raise NoMethodError end end… 

Nous avons déjà une spécification pour le nom d'utilisateur, nous n'avons donc pas besoin d'en ajouter une autre. Notez quelques choses:

  • nous appelons explicitement player.profile dans un bloc avant, sinon il sera nul lorsque nous essaierons d'obtenir la valeur d'attribut.
  • pour tester cela foo_attribute déclenche une exception, nous devons l’envelopper dans un lambda et vérifier qu’elle soulève l’erreur attendue.
  • nous testons cela identifiant équivaut à 1, comme nous le savons, c’est la valeur attendue (il s’agit d’un test purement dépendant des données).

Du point de vue de la mise en œuvre, nous pourrions définir une série de méthodes pour accéder à la profil hachage, mais cela créerait beaucoup de logique dupliquée. De plus, le résultat de l'API serait d'avoir toujours les mêmes clés.

"Nous allons compter sur method_missing pour gérer ces cas et «générer» toutes ces méthodes à la volée ".

Au lieu de cela, nous allons compter sur method_missing pour gérer ces cas et «générer» toutes ces méthodes à la volée. mais qu'est ce que ça veut dire? Sans trop de métaprogrammation, nous pouvons simplement dire que chaque fois que nous appelons une méthode non présente sur l’objet, Ruby soulève une question. NoMethodError en utilisant method_missing. En redéfinissant cette méthode dans une classe, nous pouvons modifier son comportement.

Dans notre cas, nous allons intercepter le method_missing call, vérifiez que le nom de la méthode appelée est une clé dans le hachage de profil et, en cas de résultat positif, renvoie la valeur de hachage pour cette clé. Si non, nous appellerons super élever une norme NoMethodError: cela est nécessaire pour s'assurer que notre bibliothèque se comporte exactement comme n'importe quelle autre bibliothèque. En d'autres termes, nous voulons garantir la moindre surprise possible.

Ajoutons le code suivant au Joueur classe:

 def method_missing (name, * args, & block) si profile.has_key? (name.to_s) profile [name.to_s] sinon super fin end

Le code fait exactement ce qui est décrit ci-dessus. Si vous exécutez maintenant les spécifications, vous devriez toutes les passer. Je vous encouragerais à ajouter un peu plus aux fichiers de spécifications pour un autre attribut, comme shots_count.

Cette mise en œuvre, cependant, n'est pas vraiment idiomatique Ruby. Cela fonctionne, mais il peut être rationalisé en un opérateur ternaire, une forme condensée d’un conditionnel if-else. Il peut être réécrit comme:

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

Ce n'est pas seulement une question de longueur, mais aussi de cohérence et de conventions partagées entre développeurs. Parcourir le code source des gemmes et des bibliothèques Ruby est un bon moyen de s’habituer à ces conventions..


Caching

Enfin, nous voulons nous assurer que notre bibliothèque est efficace. Il ne doit pas faire plus de demandes que nécessaire et peut-être mettre en cache les données en interne. Encore une fois, réfléchissons à la manière dont nous pourrions l’utiliser:

 player.profile => effectue la requête et retourne un hachage player.profile => retourne le même hash player.profile (true) => force le rechargement de la requête http puis renvoie le hachage (avec des modifications de données si nécessaire)

Comment pouvons-nous tester cela? Nous pouvons utiliser WebMock pour activer et désactiver les connexions réseau au point de terminaison de l'API. Même si nous utilisons des installations de magnétoscope, WebMock peut simuler un dépassement de délai réseau ou une réponse différente au serveur. Dans notre cas, nous pouvons tester la mise en cache en obtenant le profil une fois, puis en désactivant le réseau. En appelant player.profile encore une fois, nous devrions voir les mêmes données, tout en appelant player.profile (true) nous devrions avoir un Délai d'attente :: erreur, comme la bibliothèque essaierait de se connecter au point de terminaison de l'API (désactivé).

Ajoutons un autre bloc à la player_spec.rb fichier, juste après génération d'attributs dynamiques:

 décrivez "la mise en cache" # nous utilisons Webmock pour désactiver la connexion réseau # après avoir récupéré le profil avant que player.profile stub_request (: any, /api.dribbble.com/).to_timeout le termine "doit mettre en cache le profil" do player. profile.must_be_instance_of Hash end it "doit actualiser le profil s'il est forcé" do lambda player.profile (true) .must_raise Timeout :: Error end end

le demande de stub La méthode intercepte tous les appels sur le point de terminaison de l'API et simule un délai Délai d'attente :: erreur. Comme précédemment, nous testons la présence de cette erreur dans un lambda.

La mise en œuvre peut être délicate, nous allons donc la scinder en deux étapes. Tout d'abord, déplaçons la requête http réelle vers une méthode privée:

… Def profil get_profile end… private def get_profile self.class.get ("/ players / # self.username") end… 

Cela ne fera pas passer nos spécifications, car nous ne mettons pas en cache le résultat de get_profile. Pour ce faire, changeons le profil méthode:

… Def profile @profile || = get_profile end… 

Nous allons stocker le hachage de résultat dans une variable d'instance. Notez également le || = opérateur, dont la présence fait en sorte que get_profile est exécuté uniquement si @profile renvoie une valeur falsy (comme néant).

Ensuite, nous pouvons ajouter la directive de rechargement forcé:

… Def profile (force = false) force? @profile = get_profile: @profile || = get_profile end… 

Nous utilisons à nouveau un ternaire: si Obliger est faux, nous effectuons get_profile et le cache, sinon, nous utilisons la logique écrite dans la version précédente de cette méthode (c’est-à-dire exécuter la demande uniquement si nous n’avons pas déjà un hachage).

Nos spécifications devraient être vertes maintenant et c'est aussi la fin de notre tutoriel.


Emballer

Notre but dans ce tutoriel était d'écrire une petite bibliothèque efficace pour interagir avec l'API Dribbble. nous avons jeté les bases pour que cela se produise. La plupart de la logique que nous avons écrite peut être résumée et réutilisée pour accéder à tous les autres points de terminaison. Minitest, WebMock et VCR se sont avérés des outils précieux pour nous aider à façonner notre code.

.