Testen von HTTP-Anforderungen: Das Überlebenstool eines Entwicklers

Veröffentlicht: 2022-03-11

Was zu tun ist, wenn eine Testing Suite nicht durchführbar ist

Es gibt Zeiten, in denen wir – Programmierer und/oder unsere Kunden – nur begrenzte Ressourcen haben, um sowohl die erwartete Leistung als auch die automatisierten Tests für diese Leistung zu schreiben. Wenn die Anwendung klein genug ist, können Sie Abstriche machen und Tests überspringen, weil Sie sich (meistens) daran erinnern, was an anderer Stelle im Code passiert, wenn Sie eine Funktion hinzufügen, einen Fehler beheben oder umgestalten. Allerdings werden wir nicht immer mit kleinen Anwendungen arbeiten, außerdem werden sie mit der Zeit größer und komplexer. Das macht manuelles Testen schwierig und super nervig.

Bei meinen letzten Projekten war ich gezwungen, ohne automatisierte Tests zu arbeiten, und ehrlich gesagt war es peinlich, dass der Client mir nach einem Code-Push eine E-Mail mitteilte, dass die Anwendung an Stellen bricht, an denen ich nicht einmal den Code berührt habe.

In Fällen, in denen mein Kunde entweder kein Budget hatte oder beabsichtigte, ein automatisiertes Testframework hinzuzufügen, begann ich damit, die grundlegende Funktionalität der gesamten Website zu testen, indem ich eine HTTP-Anforderung an jede einzelne Seite sendete, die Antwortheader analysierte und nach der „200“ suchte. Antwort. Es klingt schlicht und einfach, aber Sie können eine Menge tun, um die Genauigkeit sicherzustellen, ohne tatsächlich Tests, Units, Funktionen oder Integrationen schreiben zu müssen.

Automatisiertes Testen

In der Webentwicklung umfassen automatisierte Tests drei Haupttesttypen: Unit-Tests, Funktionstests und Integrationstests. Wir kombinieren Unit-Tests oft mit Funktions- und Integrationstests, um sicherzustellen, dass alles als ganze Anwendung reibungslos läuft. Wenn diese Tests gemeinsam oder nacheinander (vorzugsweise mit einem einzigen Befehl oder Klick) ausgeführt werden, nennen wir sie automatisierte Tests, Einheit oder nicht.

Der Zweck dieser Tests (zumindest in der Webentwicklung) besteht hauptsächlich darin, sicherzustellen, dass alle Anwendungsseiten ohne Probleme gerendert werden, frei von schwerwiegenden (Anwendungsstopp) Fehlern oder Bugs.

Unit-Tests

Unit-Tests sind ein Softwareentwicklungsprozess, bei dem die kleinsten Teile des Codes – Units – unabhängig voneinander auf korrekte Funktion getestet werden. Hier ist ein Beispiel in 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

Funktionsprüfung

Funktionstests sind eine Technik zur Überprüfung der Merkmale und Funktionalität des Systems oder der Software, die alle Benutzerinteraktionsszenarien abdeckt, einschließlich Fehlerpfade und Grenzfälle.

Hinweis: Alle unsere Beispiele sind in Ruby.

 test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end

Integrationstests

Sobald die Module einheitengetestet sind, werden sie nacheinander integriert, um das Kombinationsverhalten zu überprüfen und zu validieren, dass die Anforderungen korrekt implementiert sind.

 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 in einer idealen Welt

Das Testen ist in der Branche weithin akzeptiert und sinnvoll; gute Tests lassen Sie:

  • Qualitätssicherung Ihrer gesamten Anwendung mit dem geringsten menschlichen Aufwand
  • Identifizieren Sie Fehler einfacher, da Sie genau wissen, wo Ihr Code aufgrund von Testfehlern beschädigt wird
  • Erstellen Sie eine automatische Dokumentation für Ihren Code
  • Vermeiden Sie „Codierverstopfung“, was laut einem Typen auf Stack Overflow eine humorvolle Art zu sagen ist: „Wenn Sie nicht wissen, was Sie als nächstes schreiben sollen, oder wenn Sie eine entmutigende Aufgabe vor sich haben, beginnen Sie damit, klein zu schreiben .“

Ich könnte weiter und weiter darüber reden, wie großartig Tests sind und wie sie die Welt verändert haben und yada yada yada, aber Sie verstehen, worauf es ankommt. Konzeptionell sind Tests großartig.

Verwandt: Komponententests, wie man testbaren Code schreibt und warum es wichtig ist

Tests in der realen Welt

Obwohl alle drei Arten von Tests Vorteile haben, werden sie in den meisten Projekten nicht geschrieben. Warum? Nun, lassen Sie es mich aufschlüsseln:

Zeit/Termine

Jeder hat Fristen, und das Schreiben frischer Tests kann dem Erfüllen einer Frist im Wege stehen. Es kann eineinhalb (oder mehr) Zeit in Anspruch nehmen, eine Bewerbung und die entsprechenden Tests zu schreiben. Nun, einige von Ihnen stimmen dem nicht zu und berufen sich letztendlich auf die eingesparte Zeit, aber ich glaube nicht, dass dies der Fall ist, und ich werde in „Meinungsverschiedenheiten“ erklären, warum.

Kundenprobleme

Oft versteht der Kunde nicht wirklich, was Testen ist oder warum es einen Wert für die Anwendung hat. Kunden sind in der Regel eher auf eine schnelle Produktbereitstellung bedacht und sehen programmatisches Testen daher als kontraproduktiv an.

Oder es kann so einfach sein, dass der Kunde nicht über das Budget verfügt, um die zusätzliche Zeit zu bezahlen, die für die Implementierung dieser Tests erforderlich ist.

Mangel an Wissen

Es gibt einen beträchtlichen Stamm von Entwicklern in der realen Welt, die nicht wissen, dass es Tests gibt. Bei jeder Konferenz, jedem Treffen, Konzert (sogar in meinen Träumen) treffe ich Entwickler, die nicht wissen, wie man Tests schreibt, nicht wissen, was man testen soll, nicht wissen, wie man das Framework zum Testen einrichtet und so weiter an. Das Testen wird in Schulen nicht gerade gelehrt, und es kann mühsam sein, das Framework einzurichten/zu lernen, um sie zum Laufen zu bringen. Also ja, es gibt eine definitive Eintrittsbarriere.

'Es ist viel Arbeit'

Das Schreiben von Tests kann sowohl für neue als auch für erfahrene Programmierer überwältigend sein, selbst für diese Weltveränderer-Genies, und um das Ganze abzurunden, ist das Schreiben von Tests nicht aufregend. Man könnte denken: „Warum sollte ich mich mit langweiliger Arbeit beschäftigen, wenn ich ein wichtiges Feature mit Ergebnissen implementieren könnte, die meinen Kunden beeindrucken werden?“ Es ist ein hartes Argument.

Nicht zuletzt ist es schwierig, Tests zu schreiben, und Informatikstudenten sind dafür nicht ausgebildet.

Oh, und Refactoring mit Unit-Tests macht keinen Spaß.

Meinungsunterschied

Unit-Tests sind meiner Meinung nach für algorithmische Logik sinnvoll, aber nicht so sehr für die Koordination von lebendem Code.

Die Leute behaupten, dass Sie, obwohl Sie im Vorfeld zusätzliche Zeit in das Schreiben von Tests investieren, später Stunden beim Debuggen oder Ändern von Code sparen. Ich bin anderer Meinung und stelle eine Frage: Ist Ihr Code statisch oder ändert er sich ständig?

Für die meisten von uns ändert es sich ständig. Wenn Sie erfolgreiche Software schreiben, fügen Sie ständig Funktionen hinzu, ändern vorhandene, entfernen sie, essen sie, was auch immer, und um diese Änderungen zu berücksichtigen, müssen Sie Ihre Tests ständig ändern, und das Ändern Ihrer Tests braucht Zeit.

Aber Sie brauchen eine Art Test

Niemand wird argumentieren, dass das Fehlen jeglicher Art von Tests der schlimmstmögliche Fall ist. Nachdem Sie Änderungen an Ihrem Code vorgenommen haben, müssen Sie bestätigen, dass er tatsächlich funktioniert. Viele Programmierer versuchen, die Grundlagen manuell zu testen: Wird die Seite im Browser gerendert? Wird das Formular übermittelt? Wird der richtige Inhalt angezeigt? Und so weiter, aber meiner Meinung nach ist das barbarisch, ineffizient und arbeitsintensiv.

Was ich stattdessen verwende

Der Zweck des manuellen oder automatisierten Testens einer Webanwendung besteht darin, zu bestätigen, dass eine bestimmte Seite im Browser des Benutzers ohne schwerwiegende Fehler gerendert wird und dass der Inhalt korrekt angezeigt wird. Eine Möglichkeit (und in den meisten Fällen eine einfachere Möglichkeit), dies zu erreichen, besteht darin, HTTP-Anforderungen an die Endpunkte der App zu senden und die Antwort zu analysieren. Der Antwortcode sagt Ihnen, ob die Seite erfolgreich zugestellt wurde. Es ist einfach, den Inhalt zu testen, indem Sie den Antworttext der HTTP-Anforderung analysieren und nach bestimmten Übereinstimmungen mit Textzeichenfolgen suchen, oder Sie können einen Schritt weiter gehen und Web-Scraping-Bibliotheken wie nokogiri verwenden.

Wenn einige Endpunkte eine Benutzeranmeldung erfordern, können Sie Bibliotheken verwenden, die für die Automatisierung von Interaktionen (ideal bei der Durchführung von Integrationstests) entwickelt wurden, z. B. für die Mechanisierung der Anmeldung oder das Klicken auf bestimmte Links. Wirklich, im Großen und Ganzen des automatisierten Testens sieht dies sehr nach Integrations- oder Funktionstests aus (je nachdem, wie Sie sie verwenden), aber es ist viel schneller zu schreiben und kann in ein vorhandenes Projekt aufgenommen oder zu einem neuen hinzugefügt werden , mit weniger Aufwand als die Einrichtung eines ganzen Testframeworks. Genau richtig!

Verwandte Themen: Stellen Sie die besten 3 % der freiberuflichen QA-Ingenieure ein.

Grenzfälle stellen ein weiteres Problem dar, wenn es um große Datenbanken mit einem breiten Wertebereich geht; Das Testen, ob unsere Anwendung über alle erwarteten Datensätze hinweg reibungslos funktioniert, kann entmutigend sein.

Eine Möglichkeit besteht darin, alle Grenzfälle vorherzusehen (was nicht nur schwierig, sondern oft unmöglich ist) und für jeden einen Test zu schreiben. Dies könnte leicht zu Hunderten von Codezeilen werden (stellen Sie sich den Horror vor) und umständlich zu warten. Mit HTTP-Anforderungen und nur einer Codezeile können Sie solche Grenzfälle jedoch direkt mit den Daten aus der Produktion testen, die lokal auf Ihren Entwicklungscomputer oder auf einen Staging-Server heruntergeladen werden.

Natürlich ist diese Testtechnik keine Wunderwaffe und hat viele Mängel, genau wie jede andere Methode, aber ich finde diese Art von Tests schneller und einfacher zu schreiben und zu modifizieren.

In der Praxis: Testen mit HTTP-Requests

Da wir bereits festgestellt haben, dass das Schreiben von Code ohne begleitende Tests keine gute Idee ist, besteht mein grundlegender Test für eine gesamte Anwendung darin, HTTP-Anforderungen lokal an alle Seiten zu senden und die Antwortheader für a zu parsen 200 (oder gewünschter) Code.

Wenn wir beispielsweise die obigen Tests (die nach bestimmten Inhalten und einem schwerwiegenden Fehler suchen) stattdessen mit einer HTTP-Anforderung (in Ruby) schreiben würden, wäre dies ungefähr so:

 # 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

Die Zeile curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) } deckt viele Testfälle ab; Jede Methode, die einen Fehler auf der Seite des Artikels auslöst, wird hier abgefangen, sodass sie effektiv Hunderte von Codezeilen in einem Test abdeckt.

Der zweite Teil, der speziell den Inhaltsfehler abfängt, kann mehrfach verwendet werden, um den Inhalt einer Seite zu überprüfen. (Komplexere Anforderungen können mit mechanize bearbeitet werden, aber das würde den Rahmen dieses Blogs sprengen.)

In Fällen, in denen Sie testen möchten, ob eine bestimmte Seite mit einem großen, unterschiedlichen Satz von Datenbankwerten funktioniert (z. B. funktioniert Ihre Artikelseitenvorlage für alle Artikel in der Produktionsdatenbank), könnten Sie Folgendes tun:

 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

Dadurch wird ein Array von IDs aller Artikel in der Datenbank zurückgegeben, die nicht gerendert wurden, sodass Sie jetzt manuell zu der jeweiligen Artikelseite gehen und das Problem überprüfen können.

Jetzt verstehe ich, dass diese Art des Testens in bestimmten Fällen möglicherweise nicht funktioniert, z. B. beim Testen eines eigenständigen Skripts oder beim Senden einer E-Mail, und sie ist unbestreitbar langsamer als Komponententests, da wir für jeden Test direkte Aufrufe an einen Endpunkt tätigen, aber wann Sie können keine Komponententests oder Funktionstests oder beides haben, das ist besser als nichts.

Wie würden Sie diese Tests strukturieren? Bei kleinen, nicht komplexen Projekten können Sie alle Ihre Tests in eine Datei schreiben und diese Datei jedes Mal ausführen, bevor Sie Ihre Änderungen festschreiben, aber die meisten Projekte erfordern eine Reihe von Tests.

Normalerweise schreibe ich zwei bis drei Tests pro Endpunkt, je nachdem, was ich teste. Sie können auch versuchen, einzelne Inhalte zu testen (ähnlich wie beim Unit-Test), aber ich denke, das wäre überflüssig und langsam, da Sie für jede Unit einen HTTP-Aufruf machen. Aber andererseits werden sie sauberer und leicht verständlich sein.

Ich empfehle, Ihre Tests in Ihrem regulären Testordner abzulegen, wobei jeder Hauptendpunkt eine eigene Datei hat (in Rails hätte beispielsweise jedes Modell/jeder Controller jeweils eine Datei), und diese Datei kann je nach unseren Angaben in drei Teile unterteilt werden testen. Ich habe oft mindestens drei Tests:

Testen Sie eins

Überprüfen Sie, ob die Seite ohne schwerwiegende Fehler zurückgegeben wird.

Test eins prüft, ob die Seite ohne schwerwiegende Fehler zurückkehrt.

Beachten Sie, wie ich eine Liste aller Endpunkte für Post erstellt und darüber iteriert habe, um zu überprüfen, ob jede Seite fehlerfrei gerendert wird. Unter der Annahme, dass alles gut gelaufen ist und alle Seiten gerendert wurden, sehen Sie im Terminal etwa Folgendes: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

Wenn eine Seite nicht gerendert wird, sehen Sie etwa Folgendes (in diesem Beispiel hat die posts/index page einen Fehler und wird daher nicht gerendert): ➜ 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”}]

Prüfung zwei

Bestätigen Sie, dass alle erwarteten Inhalte vorhanden sind:

Test zwei bestätigt, dass alle erwarteten Inhalte vorhanden sind.

Wenn alle erwarteten Inhalte auf der Seite gefunden werden, sieht das Ergebnis so aus (in diesem Beispiel stellen wir sicher, dass posts/:id einen Beitragstitel, eine Beschreibung und einen Status hat): ➜ 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 -- []

Wenn auf der Seite kein erwarteter Inhalt gefunden wird (hier erwarten wir, dass die Seite den Status des Beitrags anzeigt – „Aktiv“, wenn der Beitrag aktiv ist, „Deaktiviert“, wenn der Beitrag deaktiviert ist), sieht das Ergebnis so aus: ➜ 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”]

Prüfung drei

Überprüfen Sie, ob die Seite über alle Datensätze (falls vorhanden) gerendert wird:

Test 3 überprüft, ob die Seite über alle Datasets hinweg gerendert wird.

Wenn alle Seiten ohne Fehler gerendert werden, erhalten wir eine leere Liste: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

Wenn der Inhalt einiger Datensätze ein Problem beim Rendern hat (in diesem Beispiel geben die Seiten mit den IDs 2 und 5 einen Fehler aus), sieht das Ergebnis so aus: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

Wenn Sie mit dem obigen Demonstrationscode herumspielen möchten, hier ist mein Github-Projekt.

Was ist also besser? Es hängt davon ab, ob…

Das Testen von HTTP-Anforderungen ist möglicherweise die beste Wahl, wenn:

  • Sie arbeiten mit einer Web-App
  • Du stehst unter Zeitdruck und möchtest schnell etwas schreiben
  • Sie arbeiten mit einem großen Projekt, einem bereits bestehenden Projekt, in dem keine Tests geschrieben wurden, aber Sie möchten trotzdem eine Möglichkeit, Code zu überprüfen
  • Ihr Code beinhaltet eine einfache Anforderung und Antwort
  • Sie möchten keinen großen Teil Ihrer Zeit mit der Wartung von Tests verbringen (ich habe irgendwo Unit-Test = Wartungshölle gelesen, und ich stimme ihm/ihr teilweise zu)
  • Sie möchten testen, ob eine Anwendung mit allen Werten in einer vorhandenen Datenbank funktioniert

Herkömmliche Tests sind ideal, wenn:

  • Sie haben es mit etwas anderem als einer Webanwendung zu tun, z. B. mit Skripts
  • Sie schreiben komplexen algorithmischen Code
  • Sie haben Zeit und Budget, um Tests zu schreiben
  • Das Geschäft benötigt fehlerfreie oder eine niedrige Fehlerquote (Finanzen, große Benutzerbasis)

Vielen Dank für das Lesen des Artikels; Sie sollten jetzt eine Methode zum Testen haben, auf die Sie sich verlassen können, eine, auf die Sie sich verlassen können, wenn Sie unter Zeitdruck stehen.

Verwandt:
  • Leistung und Effizienz: Arbeiten mit HTTP/3
  • Keep It Encrypted, Keep It Safe: Arbeiten mit ESNI, DoH und DoT