Testowanie żądań HTTP: narzędzie przetrwania dla programistów

Opublikowany: 2022-03-11

Co zrobić, gdy zestaw testowy nie jest wykonalny

Czasami my – programiści i/lub nasi klienci – mamy ograniczone zasoby, za pomocą których możemy napisać zarówno oczekiwany produkt, jak i automatyczne testy tego produktu. Gdy aplikacja jest wystarczająco mała, możesz iść na skróty i pominąć testy, ponieważ pamiętasz (głównie) co dzieje się w innym miejscu kodu, gdy dodajesz funkcję, naprawiasz błąd lub refaktorujesz. To powiedziawszy, nie zawsze będziemy pracować z małymi aplikacjami, a ponadto z czasem stają się one większe i bardziej złożone. To sprawia, że ​​ręczne testowanie jest trudne i bardzo denerwujące.

W przypadku moich ostatnich kilku projektów byłem zmuszony pracować bez zautomatyzowanych testów i szczerze mówiąc, to było żenujące, gdy klient wysłał mi wiadomość e-mail po naciśnięciu kodu z informacją, że aplikacja psuje się w miejscach, w których nawet nie dotknąłem kodu.

Tak więc w przypadkach, w których mój klient nie miał ani budżetu, ani zamiaru dodania jakiegokolwiek zautomatyzowanego frameworka testowego, zacząłem testować podstawową funkcjonalność całej witryny, wysyłając żądanie HTTP do każdej strony, analizując nagłówki odpowiedzi i szukając „200”. odpowiedź. Brzmi to prosto i prosto, ale można wiele zrobić, aby zapewnić wierność bez konieczności pisania jakichkolwiek testów, jednostek, funkcjonalności lub integracji.

Testowanie automatyczne

W tworzeniu stron internetowych testy automatyczne obejmują trzy główne typy testów: testy jednostkowe, testy funkcjonalne i testy integracyjne. Często łączymy testy jednostkowe z testami funkcjonalnymi i integracyjnymi, aby upewnić się, że wszystko działa płynnie jako cała aplikacja. Kiedy te testy są wykonywane jednocześnie lub sekwencyjnie (najlepiej za pomocą jednego polecenia lub kliknięcia), zaczynamy nazywać je testami automatycznymi, jednostkowymi lub nie.

W dużej mierze celem tych testów (przynajmniej w przypadku tworzenia aplikacji internetowych) jest upewnienie się, że wszystkie strony aplikacji są renderowane bez problemów, bez krytycznych (zatrzymujących się aplikacji) błędów lub błędów.

Testów jednostkowych

Testy jednostkowe to proces tworzenia oprogramowania, w którym najmniejsze części kodu – jednostki – są niezależnie testowane pod kątem prawidłowego działania. Oto przykład w 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

Testy funkcjonalności

Testowanie funkcjonalne to technika używana do sprawdzania cech i funkcjonalności systemu lub oprogramowania, zaprojektowana w celu uwzględnienia wszystkich scenariuszy interakcji użytkownika, w tym ścieżek awarii i przypadków granicznych.

Uwaga: wszystkie nasze przykłady są w języku Ruby.

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

Testy integracyjne

Gdy moduły są testowane jednostkowo, są one integrowane jeden po drugim, sekwencyjnie, w celu sprawdzenia zachowania kombinacji i sprawdzenia, czy wymagania są poprawnie zaimplementowane.

 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

Testy w idealnym świecie

Testowanie jest powszechnie akceptowane w branży i ma sens; dobre testy pozwalają:

  • Jakość zapewnia całą aplikację przy najmniejszym wysiłku ludzkim
  • Łatwiej identyfikuj błędy, ponieważ wiesz dokładnie, gdzie Twój kod się łamie po niepowodzeniach testów
  • Stwórz automatyczną dokumentację swojego kodu
  • Unikaj „zaparć w kodowaniu”, co według niektórych gości ze Stack Overflow jest żartobliwym sposobem powiedzenia: „kiedy nie wiesz, co napisać dalej, lub masz przed sobą zniechęcające zadanie, zacznij od pisania małych ”.

Mógłbym gadać i opowiadać o tym, jak niesamowite są testy i jak zmieniły świat i bla bla bla, ale rozumiesz. Koncepcyjnie testy są niesamowite.

Powiązane: Testy jednostkowe, jak napisać testowalny kod i dlaczego ma to znaczenie

Testy w prawdziwym świecie

Chociaż wszystkie trzy typy testów mają swoje zalety, nie są one pisane w większości projektów. Czemu? Cóż, pozwól, że to wyjaśnię:

Czas/Terminy

Każdy ma terminy, a pisanie nowych testów może przeszkodzić w jego dotrzymaniu. Napisanie aplikacji i jej testów może zająć półtora (lub więcej) czasu. Teraz niektórzy z Was się z tym nie zgadzają, powołując się ostatecznie na zaoszczędzony czas, ale nie sądzę, żeby tak było i wyjaśnię dlaczego w „Różnicy opinii”.

Problemy z klientem

Często klient tak naprawdę nie rozumie, czym jest testowanie lub dlaczego ma wartość dla aplikacji. Klienci są bardziej zaniepokojeni szybką dostawą produktów i dlatego uważają, że testowanie programowe przynosi efekt przeciwny do zamierzonego.

Lub może to być tak proste, że klient nie ma budżetu na opłacenie dodatkowego czasu potrzebnego na wdrożenie tych testów.

Nieznajomość

W prawdziwym świecie istnieje spore plemię programistów, którzy nie wiedzą o istnieniu testów. Na każdej konferencji, spotkaniu, koncercie (nawet w moich snach) spotykam programistów, którzy nie wiedzą jak pisać testy, nie wiedzą co testować, nie wiedzą jak ustawić framework do testów itd. na. Testowanie nie jest dokładnie nauczane w szkołach i może być kłopotliwe skonfigurowanie/nauczenie się frameworka, aby je uruchomić. Więc tak, istnieje wyraźna bariera wejścia.

'To dużo pracy'

Pisanie testów może być przytłaczające zarówno dla nowych, jak i doświadczonych programistów, nawet dla tych geniuszy zmieniających świat, a na dodatek pisanie testów nie jest ekscytujące. Ktoś może pomyśleć: „Dlaczego miałbym angażować się w mało ekscytującą pracę, skoro mógłbym wdrażać ważną funkcję z wynikami, które zrobią wrażenie na moim kliencie?” To trudny argument.

Wreszcie trudno jest pisać testy, a studenci informatyki nie są do tego przygotowani.

Aha, a refaktoryzacja z testami jednostkowymi nie jest zabawna.

Różnica w opiniach

Moim zdaniem testowanie jednostkowe ma sens dla logiki algorytmicznej, ale nie dla koordynowania żywego kodu.

Ludzie twierdzą, że nawet jeśli inwestujesz dodatkowy czas z góry w pisanie testów, oszczędzasz godziny później podczas debugowania lub zmiany kodu. Błagam, abym się zmienił i zadaję jedno pytanie: Czy twój kod jest statyczny, czy też się zmienia?

Dla większości z nas to się ciągle zmienia. Jeśli piszesz udane oprogramowanie, zawsze dodajesz funkcje, zmieniasz istniejące, usuwasz je, jesz, cokolwiek, i aby dostosować się do tych zmian, musisz ciągle zmieniać swoje testy, a zmiana testów wymaga czasu.

Ale potrzebujesz jakiegoś rodzaju testów

Nikt nie będzie twierdził, że brak jakichkolwiek testów jest najgorszym możliwym przypadkiem. Po wprowadzeniu zmian w kodzie musisz potwierdzić, że faktycznie działa. Wielu programistów próbuje ręcznie przetestować podstawy: Czy renderowanie strony odbywa się w przeglądarce? Czy formularz jest przesyłany? Czy wyświetlana jest prawidłowa treść? I tak dalej, ale moim zdaniem jest to barbarzyńskie, nieefektywne i pracochłonne.

Czego używam zamiast tego

Celem testowania aplikacji internetowej, czy to ręcznie, czy automatycznie, jest potwierdzenie, że dana strona wyświetla się w przeglądarce użytkownika bez żadnych błędów krytycznych oraz że poprawnie wyświetla swoją zawartość. Jednym ze sposobów (i w większości przypadków prostszym) na osiągnięcie tego jest wysyłanie żądań HTTP do punktów końcowych aplikacji i analizowanie odpowiedzi. Kod odpowiedzi informuje, czy strona została pomyślnie dostarczona. Łatwo jest przetestować zawartość, analizując treść odpowiedzi żądania HTTP i wyszukując określone dopasowania ciągów tekstowych, lub możesz być bardziej wyrafinowany i korzystać z bibliotek do przeszukiwania sieci, takich jak nokogiri.

Jeśli niektóre punkty końcowe wymagają logowania użytkownika, możesz użyć bibliotek zaprojektowanych do automatyzacji interakcji (idealne podczas przeprowadzania testów integracyjnych), takich jak mechanizacja do logowania lub klikanie niektórych linków. Naprawdę, w szerokim ujęciu testów automatycznych, wygląda to jak testowanie integracyjne lub funkcjonalne (w zależności od tego, jak ich używasz), ale jest o wiele szybsze do napisania i można je włączyć do istniejącego projektu lub dodać do nowego. , przy mniejszym wysiłku niż konfigurowanie całej struktury testowej. Miejsce na!

Powiązane: Zatrudnij najlepszych 3% niezależnych inżynierów ds. kontroli jakości.

Przypadki brzegowe stanowią inny problem, gdy mamy do czynienia z dużymi bazami danych o szerokim zakresie wartości; testowanie, czy nasza aplikacja działa płynnie we wszystkich oczekiwanych zestawach danych, może być zniechęcające.

Jednym ze sposobów jest przewidzenie wszystkich skrajnych przypadków (co jest nie tylko trudne, ale często niemożliwe) i napisanie testu dla każdego z nich. Mogłoby to łatwo stać się setkami linii kodu (wyobraź sobie horror) i kłopotliwe w utrzymaniu. Jednak dzięki żądaniom HTTP i tylko jednej linii kodu można testować takie przypadki brzegowe bezpośrednio na danych z produkcji, pobranych lokalnie na maszynie deweloperskiej lub na serwerze pomostowym.

Oczywiście ta technika testowania nie jest srebrną kulą i ma wiele wad, tak samo jak każda inna metoda, ale uważam, że tego typu testy są szybsze i łatwiejsze do napisania i modyfikacji.

W praktyce: Testowanie za pomocą żądań HTTP

Ponieważ ustaliliśmy już, że pisanie kodu bez jakichkolwiek towarzyszących testów nie jest dobrym pomysłem, moim podstawowym testem dla całej aplikacji jest wysyłanie żądań HTTP do wszystkich jej stron lokalnie i analizowanie nagłówków odpowiedzi dla 200 (lub żądany) kod.

Na przykład, gdybyśmy mieli napisać powyższe testy (te szukające konkretnej treści i krytycznego błędu) z żądaniem HTTP zamiast (w Ruby), byłoby to coś takiego:

 # 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

Linia curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) } obejmuje wiele przypadków testowych; zostanie tu wychwycona każda metoda zgłaszająca błąd na stronie artykułu, dzięki czemu w jednym teście skutecznie pokrywa setki wierszy kodu.

Druga część, która wyłapuje w szczególności błąd treści, może być używana wielokrotnie do sprawdzania treści na stronie. (Bardziej złożone żądania można obsłużyć za pomocą mechanize , ale to wykracza poza zakres tego bloga).

Teraz w przypadkach, w których chcesz przetestować, czy określona strona działa na dużym, zróżnicowanym zestawie wartości bazy danych (na przykład szablon strony artykułu działa dla wszystkich artykułów w produkcyjnej bazie danych), możesz wykonać następujące czynności:

 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

Spowoduje to zwrócenie tablicy identyfikatorów wszystkich artykułów w bazie danych, które nie zostały wyrenderowane, więc teraz możesz ręcznie przejść do określonej strony artykułu i sprawdzić problem.

Teraz rozumiem, że ten sposób testowania może nie działać w niektórych przypadkach, takich jak testowanie samodzielnego skryptu lub wysyłanie wiadomości e-mail, i jest niezaprzeczalnie wolniejszy niż testy jednostkowe, ponieważ wykonujemy bezpośrednie połączenia z punktem końcowym dla każdego testu, ale kiedy nie możesz mieć testów jednostkowych, testów funkcjonalnych lub obu, to jest lepsze niż nic.

Jak byś zajął się strukturą tych testów? W przypadku małych, nieskomplikowanych projektów możesz zapisać wszystkie swoje testy w jednym pliku i uruchamiać ten plik za każdym razem przed zatwierdzeniem zmian, ale większość projektów wymaga zestawu testów.

Zwykle piszę od dwóch do trzech testów na punkt końcowy, w zależności od tego, co testuję. Możesz także spróbować przetestować poszczególne treści (podobnie do testów jednostkowych), ale myślę, że byłoby to zbędne i powolne, ponieważ będziesz wykonywał wywołanie HTTP dla każdej jednostki. Ale z drugiej strony będą czystsze i łatwe do zrozumienia.

Zalecam umieszczanie testów w zwykłym folderze testowym, gdzie każdy główny punkt końcowy ma swój własny plik (na przykład w Railsach każdy model/kontroler miałby po jednym pliku), a plik ten można podzielić na trzy części zgodnie z tym, co testują. Często mam co najmniej trzy testy:

Test pierwszy

Sprawdź, czy strona powraca bez błędów krytycznych.

Test jeden sprawdza, czy strona powraca bez żadnych błędów krytycznych.

Zwróć uwagę, jak zrobiłem listę wszystkich punktów końcowych dla Post i wykonałem iterację, aby sprawdzić, czy każda strona jest renderowana bez żadnych błędów. Zakładając, że wszystko poszło dobrze i wszystkie strony zostały wyrenderowane, w terminalu zobaczysz coś takiego: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

Jeśli jakakolwiek strona nie zostanie wyrenderowana, zobaczysz coś takiego (w tym przykładzie strona z posts/index page zawiera błąd i dlatego nie jest renderowana): ➜ 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”}]

Test drugi

Potwierdź, że jest tam cała oczekiwana zawartość:

Test drugi potwierdza, że ​​jest tam cała oczekiwana zawartość.

Jeśli cała oczekiwana treść znajduje się na stronie, wynik wygląda tak (w tym przykładzie upewniamy się, że posts/:id ma tytuł posta, opis i status): ➜ 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 -- []

Jeśli jakakolwiek oczekiwana treść nie zostanie znaleziona na stronie (tutaj oczekujemy, że strona pokaże status posta - „Aktywny”, jeśli post jest aktywny, „Wyłączony”, jeśli post jest wyłączony), wynik wygląda tak: ➜ 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 trzeci

Sprawdź, czy strona renderuje się we wszystkich zestawach danych (jeśli istnieją):

Test 3 sprawdza, czy strona renderuje się we wszystkich zestawach danych.

Jeśli wszystkie strony zostaną wyrenderowane bez żadnego błędu, otrzymamy pustą listę: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

Jeśli zawartość niektórych rekordów ma problem z renderowaniem (w tym przykładzie strony o identyfikatorach 2 i 5 dają błąd) wynik wygląda tak: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

Jeśli chcesz pobawić się powyższym kodem demonstracyjnym, oto mój projekt na github.

Więc co jest lepsze? To zależy…

Testowanie żądań HTTP może być najlepszym rozwiązaniem, jeśli:

  • Pracujesz z aplikacją internetową
  • Jesteś w kryzysie czasu i chcesz szybko coś napisać
  • Pracujesz z dużym projektem, wcześniej istniejącym projektem, w którym nie napisano testów, ale nadal chcesz sprawdzić kod
  • Twój kod zawiera proste żądanie i odpowiedź
  • Nie chcesz spędzać dużej części czasu na utrzymywaniu testów (czytałem gdzieś test jednostkowy = piekło utrzymania i częściowo się z nim/nią zgadzam)
  • Chcesz sprawdzić, czy aplikacja działa na wszystkich wartościach w istniejącej bazie danych

Tradycyjne testowanie jest idealne, gdy:

  • Masz do czynienia z czymś innym niż aplikacja internetowa, np. skryptami
  • Piszesz złożony, algorytmiczny kod
  • Masz czas i budżet, które możesz poświęcić na pisanie testów
  • Firma wymaga braku błędów lub niskiego wskaźnika błędów (finanse, duża baza użytkowników)

Dzięki za przeczytanie artykułu; powinieneś mieć teraz metodę testowania, którą możesz domyślnie zastosować, taką, na którą możesz liczyć, gdy brakuje ci czasu.

Związane z:
  • Wydajność i wydajność: praca z HTTP/3
  • Zaszyfruj, zachowaj bezpieczeństwo: praca z ESNI, DoH i DoT