
Partager:
Karl est un défenseur des développeurs pour Vonage, qui se concentre sur la maintenance de nos SDK de serveur Ruby et sur l'amélioration de l'expérience des développeurs pour notre communauté. Il aime apprendre, fabriquer des objets, partager ses connaissances et tout ce qui a trait à la technologie du web.
Tester des API externes en Ruby avec Webmock et VCR
Temps de lecture : 14 minutes
Les tests automatisés font depuis longtemps partie intégrante du processus de développement et de déploiement des logiciels, dont les avantages sont bien établis. Entre autres, nous utilisons les tests pour assurer la qualité du code, pour prévenir les régressions et, dans le contexte du développement piloté par les tests (TDD), dans le cadre du processus d'écriture du code.
En tant que développeurs, nous sommes familiers et à l'aise avec le concept d'écriture de suites de tests automatisés dans le cadre de notre flux de travail de développement. Cependant, lorsque notre application exploite des API externes, l'écriture de ces tests s'accompagne d'un ensemble particulier de défis. Examinons-en quelques-uns à l'aide d'un exemple.
Exemple : Envoi d'un SMS avec l'API Messages de Vonage
Imaginons un scénario dans lequel nous développons une application qui inclut la fonctionnalité d'envoi de messages texte par SMS. Il s'agirait d'un excellent cas d'utilisation de l'API Vonage Messages API. Pour mettre en œuvre cette fonctionnalité à l'aide de l'API Messages, notre application pourrait inclure une classe MessagesClient qui définit une méthode send_sms méthode. L'objectif de cette méthode serait d'envoyer une demande formatée de manière appropriée pour utiliser l'API Messages de Vonage. POST correctement formatée pour utiliser le point de terminaison de l'API point de terminaison de l'API Messages:
https://api.nexmo.com/v1/messages
En ce qui concerne les dépendances requises pour les tests, notre Gemfile pourrait ressembler à ceci (bien qu'en réalité, notre application inclurait probablement des dépendances supplémentaires).
Gemfile
# Gemfile
source "https://rubygems.org"
ruby "3.0.0"
gem 'faraday'
group :test do
gem 'rspec'
endLe projet Faraday est une bibliothèque Ruby permettant de gérer les requêtes et les réponses HTTP.
Notre fichier d'application définirait la classe MessagesClient avec sa méthode send_sms méthode.
app.rb
# app.rb
require "json"
class MessagesClient
URI = 'https://api.nexmo.com/v1/messages'
def send_sms(from_number, to_number, message)
headers = generate_headers(message)
body = {
message_type: 'text',
channel: 'sms',
from: from_number,
to: to_number,
text: message
}
Faraday.new.post(URI, body.to_json, headers)
end
# additional methods omitted for brevity
endDans l'exemple ci-dessus, notre méthode send_sms définit des paramètres pour le texte du message, le numéro de destinataire et le numéro de destinataire. Elle inclut ces détails dans un message body qui est envoyé dans une requête POST au point de terminaison de l'API à l'aide de la méthode de Faraday post de Faraday. La méthode send_sms renvoie ensuite un objet représentant la réponse HTTP reçue par l'instance de Faraday.
Alors, comment tester cette méthode ? Une approche pour tester le chemin heureux serait d'écrire un test qui invoque la méthode, frappant ainsi le point de terminaison de l'API en direct, et affirmant que nous recevons un code de réponse qui indique le succès. Dans le cas de la version 1 de l'API Messages, il s'agirait d'un code de réponse HTTP de 202.
messages_client_spec.rb
require "spec_helper"
describe MessagesClient do
let(:app) { MessagesClient.new }
describe "#send_sms" do
let(:from_number) { "447700900000" }
let(:to_number) { "447700900001" }
let(:message) { "Hello world!" }
it "returns status 202 Accepted" do
response = app.send_sms(from_number, to_number, message)
expect(response.status).to eq 202
end
end
endCette méthode de test pose certains problèmes.
Tout d'abord, tout test qui interagit avec une dépendance externe, telle qu'une base de données ou une API externe, est susceptible d'être beaucoup plus lent qu'un test qui n'en a pas. Si nous regardons un exemple d'exécution de ce test, nous pouvons voir qu'il prend 1.63 secondes.
Finished in 1.63 seconds (files took 0.36557 seconds to load)
1 example, 0 failuresMême si cela ne semble pas semble lent, il s'agit d'un seul test pour une seule méthode. En fonction de la taille et de la complexité de notre application, nous pourrions avoir de nombreux tests similaires. Dans un tel scénario, l'exécution de l'ensemble de la suite de tests deviendrait douloureusement lente.
Deuxièmement, étant donné que notre test repose sur l'envoi d'une requête HTTP sur le réseau et son traitement par une dépendance externe, nous ne pouvons pas être certains qu'une réponse valide sera reçue, ni même qu'il y aura une réponse du tout. Il peut y avoir des problèmes de réseau, des pannes temporaires ou d'autres facteurs externes hors de notre contrôle, ce qui signifie que nous pourrions ne pas recevoir la réponse attendue à chaque fois que chaque fois que nous exécutons notre test.
Il y a beaucoup de discussions sur le sujet des meilleures pratiques pour écrire des tests automatisés, et sur ce que les différents types de tests devraient faire et ne pas faire. Certains principes généralement acceptés, en particulier lorsque l'on travaille avec des tests unitaires et de petits tests d'intégration, sont que nous devrions essayer de faire en sorte que nos tests soient rapides et déterministes.
Plus nous descendons dans la "pyramide des tests", plus nous avons de tests et plus nous les exécutons souvent. La rapidité des tests est donc importante à ce niveau. En outre, lorsque les tests sont utilisés dans le cadre du processus de développement ou pour un retour d'information précoce, il est important qu'ils soient déterministes ; en d'autres termes, nous voulons être sûrs que, lorsqu'on lui fournit une entrée spécifique, le test doit produire une sortie prédéterminée.
L'examen de ces principes dans le contexte de notre send_sms pose un problème. Nous voulons que notre test soit rapide et déterministe, mais les problèmes liés à l'utilisation du point de terminaison de l'API en direct vont à l'encontre de ces principes.
Il existe d'autres considérations possibles lors de l'utilisation d'une API externe, telles que les méthodes HTTP non idempotentes (par exemple, la requête POST dans notre méthode créera un SMS réel à chaque fois que nous exécuterons notre test), ou des problèmes potentiels liés aux coûts ou aux limites tarifaires. Bon nombre de ces problèmes supplémentaires pourraient être résolus par l'utilisation d'un bac à sable pour API, et l'API Messages fournit un bac à sable pour certains canaux de messagerie.
L'utilisation d'un bac à sable est plus pertinente pour les tests situés plus haut dans la pyramide, tels que les tests de bout en bout ou les tests fonctionnels et certains tests d'intégration plus importants, et ne résout pas vraiment nos problèmes de vitesse et de déterminisme. Ce que nous voulons vraiment pour nos tests de niveau inférieur, c'est un moyen de de ne pas toucher du tout la dépendance externe. Une solution à cela est mocking.
Moquerie
Pour ceux qui ne sont pas familiers avec le terme, le mocking est une technique de test par laquelle une réponse fictive ou "fausse" est utilisée comme substitut d'une réponse réelle provenant d'une partie interne ou externe de notre application. Au niveau interne, il peut s'agir d'un objet fictif renvoyé par une méthode ou un appel de fonction. Dans le contexte des API externes, il s'agira généralement de remplacer une réponse HTTP réelle par une réponse fictive.
L'un des grands avantages de cette approche lorsque l'on travaille avec une API externe est qu'en n'envoyant pas de requête sur le réseau et en n'attendant pas la réponse, l'exécution d'un test devient beaucoup plus rapide. De plus, en prédéfinissant la réponse HTTP, nous rendons nos tests déterministes.
L'imitation semble être la solution idéale pour résoudre les problèmes que nous avons identifiés avec notre configuration de test actuelle, alors comment pouvons-nous la mettre en œuvre dans nos send_sms test ?
Présentation de Webmock
Webmock est une bibliothèque Ruby permettant de créer des attentes sur les requêtes HTTP. Elle prend en charge un certain nombre de bibliothèques HTTP différentes, et peut être intégrée dans divers cadres de test, notamment rspec.
En tant que modèle mental de haut niveau, Webmock fait essentiellement ce qui suit :
Intercepte toutes les requêtes HTTP sortantes effectuées par notre application
Ces demandes sont comparées à un "stub" préenregistré.
Renvoie une réponse prédéfinie pour cette demande, à la place de la réponse HTTP réelle.
Explorons ce modèle mental en action en ajoutant Webmock à notre configuration de test.
Exemple mis à jour : Mocker nos réponses HTTP avec Webmock
Tout d'abord, nous devons ajouter webmock à notre Gemfile et exécuter bundle install.
Gemfile
# Gemfile
source "https://rubygems.org"
ruby "3.0.0"
gem 'faraday'
group :test do
gem 'rspec'
gem 'webmock'
endRemarque : nous devons également ajouter require 'webmock/rspec' à notre spec_helper.
Nous pouvons ensuite mettre à jour notre test pour utiliser Webmock.
messages_client_spec.rb
require "spec_helper"
describe MessagesClient do
let(:app) { MessagesClient.new }
describe "#send_sms" do
let(:from_number) { "447700900000" }
let(:to_number) { "447700900001" }
let(:message) { "Hello world!" }
it "returns status 202 Accepted" do
stub_request(:post, "https://api.nexmo.com/v1/messages").to_return(status: 202)
response = app.send_sms(from_number, to_number, message)
expect(response.status).to eq 202
end
end
endNotre test mis à jour enregistre un stuben utilisant la méthode stub_request de Webmock. Le stub est configuré pour correspondre à n'importe quelle POST à https://api.nexmo.com/v1/messageset retourner un :status de 202.
Webmock intercepte la requête HTTP sortante et renvoie à la place la réponse prédéterminée, de sorte que l'exécution de notre test est beaucoup beaucoup plus rapide qu'auparavant :
Finished in 0.00749 seconds (files took 0.50786 seconds to load)
1 example, 0 failures Limites de la simulation
L'ajout de Webmock a rendu notre test rapide et déterministe, et semble donc convenir à nos besoins de test. L'utilisation d'un outil de mocking pour tester des API externes s'accompagne toutefois de quelques mises en garde.
L'écriture de Mocks peut prendre beaucoup de temps
Notre exemple de mock ne définit que le code de la réponse. :status pour la réponse. D'autres mocks pourraient avoir besoin de définir également la réponse :headers et/ ou :body. En fonction de l'API testée, ces en-têtes et ce corps peuvent être assez volumineux et complexes, et nécessiter un certain temps pour être définis dans l'émulation.
Si l'on multiplie les tests sur plusieurs points d'extrémité d'API, l'écriture de ces mocks représente un investissement en temps considérable.
Les Mocks peuvent être difficiles à maintenir
En rapport avec ce premier point, il y a l'entretien. Les API externes peuvent évoluer au fil du temps, en ajoutant de nouvelles fonctionnalités ou en publiant de nouvelles versions. Par exemple, l'API Messages API de Vonage de Vonage a récemment publié une nouvelle version. Si nous avons un grand nombre de mocks complexes pour nos tests, la maintenance de ces mocks pour suivre les changements peut prendre beaucoup de temps et d'efforts qui pourraient être mieux investis ailleurs.
Les Mocks peuvent faire des suppositions incorrectes sur les dépendances
Puisque les mocks sont écrits pour représenter une réponse particulière plutôt que d'être une réponse elles sont nécessairement basées sur des hypothèses concernant la réponse réelle. Ce n'est peut-être pas un problème pour notre exemple de test, mais au fur et à mesure que les réponses que nous voulons simuler augmentent en complexité, la possibilité de faire une hypothèse incorrecte sur cette réponse augmente également. De telles suppositions incorrectes pourraient conduire à un code qui passe le test simulé mais qui ne fonctionne pas correctement. Il est à espérer que des tests plus élevés dans la pyramide permettront d'identifier de tels problèmes, mais idéalement, nous voulons les détecter le plus tôt possible avec nos tests de niveau inférieur.
Pour remédier à ces limitations, nous pouvons nous tourner vers une autre bibliothèque Ruby : VCR.
MAGNÉTOSCOPE
VCR est une bibliothèque qui suit le modèle de test "enregistrement et relecture" dans le contexte des requêtes et des réponses HTTP. Essentiellement, elle enregistre les interactions HTTP d'une suite de tests et les rejoue lors des prochains tests.
Le magnétoscope met en œuvre ce modèle en utilisant l'idée de "cassettes" (basée sur les cassettes des anciens magnétoscopes). Video Cassette Recorder obsolète). Chaque "cassette" est un fichier contenant des données qui représentent l'enregistrement d'une interaction HTTP spécifique. La première fois qu'un test est exécuté, un cycle réel de demande/réponse HTTP se produit et les détails de ce cycle sont enregistrés sous forme de cassette.
La cassette contient des informations sur la demande et la réponse. Les données relatives à la demande sont utilisées pour faire correspondre les demandes lors des tests ultérieurs, et les données relatives à la réponse sont utilisées pour simuler la réponse attendue lors de ces tests. Étant donné que les "simulacres" utilisent les données réelles réelles, cela permet de résoudre les problèmes évoqués plus haut concernant le temps nécessaire à l'écriture et à la maintenance des simulacres, ainsi que les hypothèses formulées à cette occasion.
Ce mode de fonctionnement du magnétoscope peut être plus facile à visualiser si nous l'examinons dans le contexte de notre dispositif d'essai.
Exemple actualisé : Intégration d'un magnétoscope dans notre dispositif de test
Nous devons d'abord ajouter vcr à notre Gemfile et lancer bundle install
Gemfile
# Gemfile
source "https://rubygems.org"
ruby "3.0.0"
gem 'faraday'
group :test do
gem 'rspec'
gem 'webmock'
gem 'vcr'
endNous pouvons alors mettre à jour notre test pour utiliser le magnétoscope.
messages_client_spec.rb
require "spec_helper"
require "vcr"
VCR.configure do |config|
config.cassette_library_dir = "spec/cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
end
describe MessagesClient do
let(:app) { MessagesClient.new }
describe "#send_sms" do
let(:from_number) { "447700900000" }
let(:to_number) { "447700900001" }
let(:message) { "Hello world!" }
it "returns status 202 Accepted" do
response = VCR.use_cassette('send_sms') do
app.send_sms(from_number, to_number, message)
end
expect(response.status).to eq 202
end
end
endDans ce fichier, nous demandons vcr puis nous configurons notre magnétoscope (si nous avions plusieurs fichiers de spécifications, nous pourrions déplacer la configuration dans notre fichier spec_helper ).
La configuration
cassette_library_dirindique au magnétoscope où stocker les "cassettes". Nous spécifions ici un répertoirespec/cassettesSi ce répertoire n'existe pas, le magnétoscope le créera.La configuration
hook_intoindique à VCR comment se connecter aux requêtes HTTP. Puisque nous avons déjàwebmockcomme dépendance, nous le spécifions ici (bien que nous puissions l'enlever de notre fichierGemfileet se brancher directement sur Faraday à la place).
Il existe également de nombreuses d'autres options de configuration disponibles.
Dans le test lui-même, nous avons supprimé la requête stubbed de Webmock. A la place, nous avons mis la variable response à la valeur de retour de la méthode use_cassette à laquelle nous transmettons un nom de cassette comme argument ainsi qu'un bloc. Dans la configuration d'enregistrement par défaut, si la cassette existe, VCR l'utilisera pour construire un objet de réponse que nous pourrons ensuite évaluer dans notre test. Si la cassette n'existe pas, VCR appellera le bloc et utilisera ce qu'il renvoie pour créer la cassette.
Lorsque nous premier exécuter notre test, puisque la cassette n'existe pas à ce stade, nous atteignons le point de terminaison de l'API et le magnétoscope utilise cette interaction HTTP pour créer un fichier send_sms.yml qui stocke les détails de la requête et de la réponse HTTP.
send_sms.yml
---
http_interactions:
- request:
method: post
uri: https://api.nexmo.com/v1/messages
body:
encoding: UTF-8
string: '{"message_type":"text","channel":"sms","from":"447700900000","to":"447700900001","text":"Hello
world!"}'
headers:
User-Agent:
- Faraday v1.8.0
Authorization:
- Basic xxxxxxxxxxxxxxxxxxxxxxxxxxx==
Content-Type:
- application/json
Host:
- api.nexmo.com
Content-Length:
- '12'
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 202
message: Accepted
headers:
Server:
- nginx
Date:
- Thu, 18 Nov 2021 12:35:49 GMT
Content-Type:
- application/json
Content-Length:
- '55'
body:
encoding: UTF-8
string: '{"message_uuid":"c26213be-2916-4c64-903e-4125158eedd8"}'
recorded_at: Thu, 18 Nov 2021 12:35:49 GMT
recorded_with: VCR 6.0.0Lorsque le test est exécuté pour la première fois, étant donné que nous touchons le point de terminaison de l'API, l'exécution est aussi lente que si nous n'utilisions pas du tout Webmock ou VCR.
Finished in 1.61 seconds (files took 0.58899 seconds to load)
1 example, 0 failuresPour tous les suivants le magnétoscope utilisera la cassette. Il vérifie d'abord que les détails de la demande pour l'exécution du test correspondent à ceux enregistrés dans la cassette. Si c'est le cas correspondent correspondent, les détails de la réponse de la cassette seront utilisés pour créer un objet de réponse. Dans ce cas, le code de réponse enregistré est 202 qui sera donc défini comme le code status pour notre objet de réponse. Puisque notre test affirme que response.status doit être égal à 202notre test passe.
Ces tests ultérieurs sont également beaucoup beaucoup plus rapides que les premiers.
Finished in 0.01033 seconds (files took 0.55884 seconds to load)
1 example, 0 failures Trucs et astuces sur les magnétoscopes
Le magnétoscope offre une grande flexibilité en termes d'options de configuration.
Configurer la correspondance des demandes
Pour rejouer un enregistrement, le magnétoscope doit faire correspondre les nouvelles demandes HTTP aux détails d'un enregistrement antérieur. Cette correspondance peut se faire sur la base de différents éléments d'une requête.
La configuration par défaut consiste à vérifier la méthode HTTP et l'URI, mais cette configuration peut être modifiée pour vérifier l'hôte et le chemin séparément (plutôt que l'URI complet), les paramètres de la requête, les en-têtes de la requête et le corps de la requête.
En outre, il est possible de créer des outils d'appariement personnalisés afin d'accroître encore la flexibilité.
Réenregistrer, ne pas disparaître
Comme indiqué précédemment, les API externes peuvent changer au fil du temps ou publier de nouvelles versions, ce qui signifie que les enregistrements peuvent devenir "obsolètes". Si cela se produit, plutôt que de devoir réécrire un mock entier (comme nous le ferions avec une configuration de mocking standard), nous pouvons enregistrer une nouvelle interaction HTTP pour remplacer celle qui est périmée. Il y a plusieurs façons d'aborder le réenregistrement :
La méthode la plus brutale consiste à supprimer le fichier de l'enregistrement en cours. Si aucun enregistrement n'existe pour un test spécifique, le magnétoscope en enregistrera automatiquement un nouveau.
Il existe également plusieurs modes d'enregistrement qui peuvent être définis pour déterminer quand de nouveaux enregistrements sont effectués. Par exemple, le mode par défaut n'enregistre les nouvelles interactions que s'il n'y a pas de fichier cassette,
:once(qui est le mode par défaut) n'enregistre de nouvelles interactions que s'il n'y a pas de fichier cassette, tandis que:new_episodesenregistre une nouvelle interaction s'il existe un fichier pour un test mais que les détails de la demande pour ce test ne correspondent pas exactement à ceux enregistrés dans le fichier.Nous pouvons activer le réenregistrement automatique afin de réenregistrer les interactions à intervalles réguliers. Nous pouvons définir l'option
:re_record_intervaldans le setup pour une cassette particulière. Lorsque cette cassette est utilisée, le magnétoscope vérifie l'horodatage de la cassette par rapport à l'heure actuelle.recorded_atl'horodatage de la cassette par rapport à l'heure actuelle. S'il s'est écoulé plus de temps que ce qui est spécifié par l'option:re_record_intervall'interaction sera réenregistrée.
Attention aux données sensibles
Lors de l'interaction avec une API externe, en fonction de la méthode d'authentification de cette API, il se peut que nous incluions des données sensibles telles que des clés API dans nos demandes, par exemple dans un fichier d'autorisation. autorisation . Ces données seront présentes dans l'enregistrement du magnétoscope dans le cadre de l'interaction. Si nous mettons également le code de notre projet à la disposition du public, par exemple en le plaçant dans un dépôt public sur GitHub, cela peut poser un problème.
Une solution pourrait consister à ajouter des enregistrements individuels, ou l'ensemble de notre répertoire, à une base de données. cassettes dans un fichier .gitignore fichier. Une autre solution consiste à utiliser l'option de configuration filter_sensitive_data option de configuration du magnétoscope pour spécifier une chaîne de substitution pour certaines données, qui sera affichée dans l'enregistrement à la place des données réelles.
Utiliser la documentation
Le VCR fournit une documentation d'utilisation détaillée pour ces options de configuration, et bien d'autres, ainsi qu'une documentation de l'API pour la bibliothèque elle-même.
Outils alternatifs
Les outils présentés ici sont bien établis dans l'écosystème Ruby, mais il existe également de nombreuses alternatives, tant pour les rubyistes que pour les non-rubyistes.
En termes de capacités d'imitation, la rspec-mocks bibliothèque peut fournir des capacités d'imitation à rspec. Certaines bibliothèques HTTP telles que Faraday fournissent des adaptateurs qui vous permettent de définir des requêtes stubbed. La fonctionnalité de mocking de Faraday (entre autres) est également compatible avec VCR.
En outre, il existe de nombreux portages de VCR vers d'autres langages de programmation, tels que vcrpy pour Python, php-vcr pour PHP, et scotch et Betamax.Net pour .net/ C#. Nock fournit des fonctionnalités similaires à la combinaison VCR et Webmock pour Node.
Il existe de nombreux autres ports vers d'autres langues énumérées dans le VCR READMEdonc quelle que soit la langue que vous utilisez, il devrait y avoir un outil d'enregistrement et de relecture à votre disposition.
Bon test !
Partager:
Karl est un défenseur des développeurs pour Vonage, qui se concentre sur la maintenance de nos SDK de serveur Ruby et sur l'amélioration de l'expérience des développeurs pour notre communauté. Il aime apprendre, fabriquer des objets, partager ses connaissances et tout ce qui a trait à la technologie du web.