Test de requête HTTP : un outil de survie pour les développeurs
Publié: 2022-03-11Que faire lorsqu'une suite de tests n'est pas réalisable
Il y a des moments où nous – programmeurs et/ou nos clients – avons des ressources limitées pour écrire à la fois le livrable attendu et les tests automatisés pour ce livrable. Lorsque l'application est suffisamment petite, vous pouvez couper les coins ronds et ignorer les tests car vous vous souvenez (principalement) de ce qui se passe ailleurs dans le code lorsque vous ajoutez une fonctionnalité, corrigez un bogue ou refactorisez. Cela dit, nous ne travaillerons pas toujours avec de petites applications, de plus, elles ont tendance à devenir plus grandes et plus complexes avec le temps. Cela rend les tests manuels difficiles et super ennuyeux.
Pour mes derniers projets, j'ai été obligé de travailler sans tests automatisés et honnêtement, c'était embarrassant que le client m'envoie un e-mail après une poussée de code pour dire que l'application se cassait à des endroits où je n'avais même pas touché au code.
Ainsi, dans les cas où mon client n'avait ni budget ni intention d'ajouter un framework de test automatisé, j'ai commencé à tester les fonctionnalités de base de l'ensemble du site Web en envoyant une requête HTTP à chaque page individuelle, en analysant les en-têtes de réponse et en recherchant le '200' réponse. Cela semble clair et simple, mais vous pouvez faire beaucoup pour garantir la fidélité sans avoir à écrire de tests, d'unité, de fonctionnalité ou d'intégration.
Tests automatisés
Dans le développement Web, les tests automatisés comprennent trois principaux types de tests : les tests unitaires, les tests fonctionnels et les tests d'intégration. Nous combinons souvent des tests unitaires avec des tests fonctionnels et d'intégration pour nous assurer que tout fonctionne correctement dans l'ensemble de l'application. Lorsque ces tests sont exécutés à l'unisson, ou séquentiellement (de préférence avec une seule commande ou un seul clic), nous commençons à les appeler tests automatisés, unitaires ou non.
En grande partie, le but de ces tests (au moins dans le développement Web) est de s'assurer que toutes les pages de l'application sont rendues sans problème, sans erreurs ou bogues fatals (arrêt de l'application).
Tests unitaires
Le test unitaire est un processus de développement logiciel dans lequel les plus petites parties du code - les unités - sont testées indépendamment pour un fonctionnement correct. Voici un exemple en Ruby :
test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end
Test fonctionel
Les tests fonctionnels sont une technique utilisée pour vérifier les caractéristiques et les fonctionnalités du système ou du logiciel, conçues pour couvrir tous les scénarios d'interaction avec l'utilisateur, y compris les chemins de défaillance et les cas limites.
Remarque : tous nos exemples sont en Ruby.
test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end
Tests d'intégration
Une fois les modules testés unitairement, ils sont intégrés un par un, séquentiellement, pour vérifier le comportement combinatoire, et valider que les exigences sont correctement implémentées.
test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end
Tests dans un monde idéal
Les tests sont largement acceptés dans l'industrie et cela a du sens; de bons tests vous permettent :
- La qualité assure l'ensemble de votre application avec le moindre effort humain
- Identifiez les bogues plus facilement, car vous savez exactement où votre code se brise à partir des échecs de test
- Créez une documentation automatique pour votre code
- Évitez la «constipation de codage», qui, selon certains mecs sur Stack Overflow, est une façon humoristique de dire: «lorsque vous ne savez pas quoi écrire ensuite, ou que vous avez une tâche ardue devant vous, commencez par écrire petit .”
Je pourrais continuer encore et encore sur la façon dont les tests sont géniaux et comment ils ont changé le monde et yada yada yada, mais vous comprenez. Conceptuellement, les tests sont géniaux.
Tests dans le monde réel
Bien qu'il y ait des mérites aux trois types de tests, ils ne sont pas écrits dans la plupart des projets. Pourquoi? Eh bien, laissez-moi le décomposer:
Temps/Délai
Tout le monde a des échéances, et écrire de nouveaux tests peut vous empêcher d'en respecter une. Cela peut prendre un temps et demi (ou plus) pour écrire une application et ses tests respectifs. Maintenant, certains d'entre vous ne sont pas d'accord avec cela, citant le temps gagné en fin de compte, mais je ne pense pas que ce soit le cas et j'expliquerai pourquoi dans 'Différence d'opinion'.
Problèmes des clients
Souvent, le client ne comprend pas vraiment ce qu'est le test ou pourquoi il a de la valeur pour l'application. Les clients ont tendance à être plus préoccupés par la livraison rapide des produits et considèrent donc les tests programmatiques comme contre-productifs.
Ou, cela peut être aussi simple que le client n'a pas le budget pour payer le temps supplémentaire nécessaire à la mise en œuvre de ces tests.
Manque de connaissances
Il existe une importante tribu de développeurs dans le monde réel qui ne sait pas que les tests existent. A chaque conférence, meetup, concert, (même dans mes rêves), je rencontre des développeurs qui ne savent pas comment écrire des tests, ne savent pas quoi tester, ne savent pas comment configurer le framework pour les tests, etc. au. Les tests ne sont pas exactement enseignés dans les écoles, et il peut être fastidieux de configurer/d'apprendre le cadre pour les faire fonctionner. Alors oui, il y a une barrière bien définie à l'entrée.
"C'est beaucoup de travail"
L'écriture de tests peut être écrasante pour les programmeurs débutants et expérimentés, même pour les génies qui changent le monde, et pour couronner le tout, écrire des tests n'est pas excitant. On peut penser : "Pourquoi devrais-je m'engager dans un travail fastidieux et sans intérêt alors que je pourrais mettre en œuvre une fonctionnalité majeure avec des résultats qui impressionneront mon client ?" C'est un argument difficile.
Enfin, il est difficile d'écrire des tests et les étudiants en informatique ne sont pas formés pour cela.
Oh, et refactoriser avec des tests unitaires n'est pas amusant.
Différence d'opinion
À mon avis, les tests unitaires ont du sens pour la logique algorithmique, mais pas tant pour la coordination du code vivant.
Les gens affirment que même si vous investissez du temps supplémentaire dans l'écriture de tests, cela vous fait gagner des heures plus tard lors du débogage ou de la modification du code. Je vous prie de différer et de poser une question : votre code est-il statique ou en constante évolution ?
Pour la plupart d'entre nous, cela change constamment. Si vous écrivez un logiciel réussi, vous ajoutez toujours des fonctionnalités, modifiez celles qui existent, les supprimez, les mangez, peu importe, et pour vous adapter à ces changements, vous devez continuer à modifier vos tests, et modifier vos tests prend du temps.
Mais, vous avez besoin d'une sorte de test
Personne ne dira que l'absence de toute sorte de test est le pire des cas possibles. Après avoir apporté des modifications à votre code, vous devez confirmer qu'il fonctionne réellement. De nombreux programmeurs essaient de tester manuellement les bases : la page s'affiche-t-elle dans le navigateur ? Le formulaire est-il soumis ? Le contenu correct est-il affiché ? Et ainsi de suite, mais à mon avis, c'est barbare, inefficace et laborieux.
Ce que j'utilise à la place
Le but du test d'une application Web, qu'il soit manuel ou automatisé, est de confirmer qu'une page donnée s'affiche dans le navigateur de l'utilisateur sans erreur fatale et qu'elle affiche correctement son contenu. Un moyen (et dans la plupart des cas, un moyen plus simple) d'y parvenir consiste à envoyer des requêtes HTTP aux points de terminaison de l'application et à analyser la réponse. Le code de réponse vous indique si la page a été livrée avec succès. Il est facile de tester le contenu en analysant le corps de la réponse de la requête HTTP et en recherchant des correspondances de chaînes de texte spécifiques, ou vous pouvez être un peu plus fantaisiste et utiliser des bibliothèques de grattage Web telles que nokogiri.
Si certains terminaux nécessitent une connexion utilisateur, vous pouvez utiliser des bibliothèques conçues pour automatiser les interactions (idéales lors de tests d'intégration) telles que mécaniser pour se connecter ou cliquer sur certains liens. Vraiment, dans la grande image des tests automatisés, cela ressemble beaucoup à des tests d'intégration ou fonctionnels (selon la façon dont vous les utilisez), mais c'est beaucoup plus rapide à écrire et peut être inclus dans un projet existant, ou ajouté à un nouveau , avec moins d'efforts que la mise en place d'un cadre de test complet. Spot sur!

Les cas extrêmes présentent un autre problème lorsqu'il s'agit de grandes bases de données avec une large gamme de valeurs ; tester si notre application fonctionne correctement sur tous les ensembles de données prévus peut être intimidant.
Une façon de procéder est d'anticiper tous les cas extrêmes (ce qui n'est pas simplement difficile, c'est souvent impossible) et d'écrire un test pour chacun. Cela pourrait facilement devenir des centaines de lignes de code (imaginez l'horreur) et fastidieux à maintenir. Pourtant, avec des requêtes HTTP et une seule ligne de code, vous pouvez tester ces cas extrêmes directement sur les données de production, téléchargées localement sur votre machine de développement ou sur un serveur intermédiaire.
Bien sûr, cette technique de test n'est pas une solution miracle et présente de nombreuses lacunes, comme toute autre méthode, mais je trouve ces types de tests plus rapides et plus faciles à écrire et à modifier.
En pratique : tester avec des requêtes HTTP
Puisque nous avons déjà établi qu'écrire du code sans aucun type de test d'accompagnement n'est pas une bonne idée, mon test de base pour une application entière consiste à envoyer des requêtes HTTP à toutes ses pages localement et à analyser les en-têtes de réponse pour un 200
(ou code souhaité).
Par exemple, si nous devions écrire les tests ci-dessus (ceux qui recherchent un contenu spécifique et une erreur fatale) avec une requête HTTP à la place (en Ruby), ce serait quelque chose comme ceci :
# testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end
La ligne curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }
couvre un grand nombre de cas de test ; toute méthode générant une erreur sur la page de l'article sera interceptée ici, elle couvre donc efficacement des centaines de lignes de code en un seul test.
La deuxième partie, qui détecte spécifiquement l'erreur de contenu, peut être utilisée plusieurs fois pour vérifier le contenu d'une page. (Des requêtes plus complexes peuvent être traitées à l'aide de mechanize
, mais cela dépasse le cadre de ce blog.)
Désormais, dans les cas où vous souhaitez tester si une page spécifique fonctionne sur un ensemble important et varié de valeurs de base de données (par exemple, votre modèle de page d'article fonctionne pour tous les articles de la base de données de production), vous pouvez :
ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids
Cela renverra un tableau d'ID de tous les articles de la base de données qui n'ont pas été rendus, vous pouvez donc maintenant accéder manuellement à la page d'article spécifique et vérifier le problème.
Maintenant, je comprends que cette façon de tester peut ne pas fonctionner dans certains cas, comme tester un script autonome ou envoyer un e-mail, et c'est indéniablement plus lent que les tests unitaires car nous faisons des appels directs à un point de terminaison pour chaque test, mais quand vous ne pouvez pas avoir de tests unitaires, ou de tests fonctionnels, ou les deux, c'est mieux que rien.
Comment procéderiez-vous pour structurer ces tests ? Avec de petits projets non complexes, vous pouvez écrire tous vos tests dans un seul fichier et exécuter ce fichier à chaque fois avant de valider vos modifications, mais la plupart des projets nécessiteront une suite de tests.
J'écris généralement deux à trois tests par point final, en fonction de ce que je teste. Vous pouvez également essayer de tester un contenu individuel (similaire aux tests unitaires), mais je pense que ce serait redondant et lent puisque vous ferez un appel HTTP pour chaque unité. Mais, d'un autre côté, ils seront plus propres et faciles à comprendre.
Je recommande de placer vos tests dans votre dossier de test habituel avec chaque point final majeur ayant son propre fichier (dans Rails, par exemple, chaque modèle/contrôleur aurait un fichier chacun), et ce fichier peut être divisé en trois parties selon ce que nous testent. J'ai souvent au moins trois tests :
Testez un
Vérifiez que la page revient sans aucune erreur fatale.
Notez comment j'ai fait une liste de tous les points de terminaison pour Post
et l'ai itérée pour vérifier que chaque page est rendue sans aucune erreur. En supposant que tout s'est bien passé et que toutes les pages ont été rendues, vous verrez quelque chose comme ceci dans le terminal : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []
Si une page n'est pas rendue, vous verrez quelque chose comme ceci (dans cet exemple, la posts/index page
a une erreur et n'est donc pas rendue) : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]
Essai Deux
Confirmez que tout le contenu attendu est là :
Si tout le contenu que nous attendons est trouvé sur la page, le résultat ressemble à ceci (dans cet exemple, nous nous assurons que posts/:id
a un titre de publication, une description et un statut) : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []
Si un contenu attendu n'est pas trouvé sur la page (ici, nous nous attendons à ce que la page affiche le statut de la publication - 'Active' si la publication est active, 'Disabled' si la publication est désactivée), le résultat ressemble à ceci : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]
Test trois
Vérifiez que la page s'affiche dans tous les ensembles de données (le cas échéant) :
Si toutes les pages sont rendues sans aucune erreur, nous obtiendrons une liste vide : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []
Si le contenu de certains des enregistrements a un problème de rendu (dans cet exemple, les pages avec les ID 2 et 5 donnent une erreur), le résultat ressemble à ceci : ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]
Si vous voulez jouer avec le code de démonstration ci-dessus, voici mon projet github.
Alors, quel est le meilleur ? Ça dépend…
Le test de requête HTTP peut être votre meilleur pari si :
- Vous travaillez avec une application Web
- Vous êtes pressé par le temps et vous voulez écrire quelque chose rapidement
- Vous travaillez avec un gros projet, un projet préexistant où les tests n'ont pas été écrits, mais vous voulez toujours un moyen de vérifier le code
- Votre code implique une demande et une réponse simples
- Vous ne voulez pas passer une grande partie de votre temps à maintenir les tests (j'ai lu quelque part test unitaire = enfer de la maintenance, et je suis partiellement d'accord avec lui/elle)
- Vous souhaitez tester si une application fonctionne sur toutes les valeurs d'une base de données existante
Les tests traditionnels sont idéaux lorsque :
- Vous avez affaire à autre chose qu'une application Web, comme des scripts
- Vous écrivez un code algorithmique complexe
- Vous avez du temps et un budget à consacrer à la rédaction de tests
- L'entreprise exige une absence de bogue ou un faible taux d'erreur (finance, large base d'utilisateurs)
Merci d'avoir lu l'article; vous devriez maintenant avoir une méthode de test par défaut, sur laquelle vous pouvez compter lorsque vous êtes pressé par le temps.
- Performances et efficacité : travailler avec HTTP/3
- Gardez-le crypté, gardez-le en sécurité : Travailler avec ESNI, DoH et DoT