Testarea solicitărilor HTTP: Instrumentul de supraviețuire al dezvoltatorului

Publicat: 2022-03-11

Ce să faceți când o suită de testare nu este fezabilă

Există momente în care noi – programatorii și/sau clienții noștri – avem resurse limitate cu care să scriem atât livrabilul așteptat, cât și testele automate pentru acel livrabil. Când aplicația este suficient de mică, puteți tăia colțuri și sări peste teste pentru că vă amintiți (în mare parte) ce se întâmplă în altă parte a codului când adăugați o caracteristică, remediați o eroare sau refactorați. Acestea fiind spuse, nu vom lucra întotdeauna cu aplicații mici, în plus, acestea tind să devină mai mari și mai complexe în timp. Acest lucru face testarea manuală dificilă și foarte enervantă.

Pentru ultimele mele proiecte, am fost forțat să lucrez fără testare automată și, sincer, a fost jenant să îmi trimită un e-mail client după o apăsare de cod pentru a spune că aplicația se sparge în locuri în care nici măcar nu atinsesem codul.

Așadar, în cazurile în care clientul meu nu avea niciun buget sau intenția de a adăuga vreun cadru de testare automatizat, am început să testez funcționalitatea de bază a întregului site web, trimițând o solicitare HTTP fiecărei pagini individuale, analizând antetele răspunsului și căutând „200” raspuns. Sună simplu și simplu, dar puteți face multe pentru a asigura fidelitatea fără a fi nevoie să scrieți teste, unități, funcționale sau integrare.

Testare automată

În dezvoltarea web, testele automate cuprind trei tipuri de teste majore: teste unitare, teste funcționale și teste de integrare. Adesea combinăm testele unitare cu teste funcționale și de integrare pentru a ne asigura că totul funcționează fără probleme ca o întreagă aplicație. Când aceste teste sunt executate la unison, sau secvenţial (de preferinţă cu o singură comandă sau clic), începem să le numim teste automate, unitare sau nu.

În mare parte, scopul acestor teste (cel puțin în dezvoltarea web) este de a se asigura că toate paginile aplicației sunt redate fără probleme, fără erori sau erori fatale (oprirea aplicației).

Testarea unitară

Testarea unitară este un proces de dezvoltare software în care cele mai mici părți ale codului – unitățile – sunt testate independent pentru funcționarea corectă. Iată un exemplu în 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

Testare funcțională

Testarea funcțională este o tehnică folosită pentru a verifica caracteristicile și funcționalitatea sistemului sau a software-ului, concepută pentru a acoperi toate scenariile de interacțiune cu utilizatorul, inclusiv căile de defecțiuni și cazurile limită.

Notă: toate exemplele noastre sunt în Ruby.

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

Testare de integrare

Odată ce modulele sunt testate unitar, acestea sunt integrate unul câte unul, secvenţial, pentru a verifica comportamentul combinaţional şi pentru a valida dacă cerinţele sunt implementate corect.

 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

Teste într-o lume ideală

Testarea este acceptată pe scară largă în industrie și are sens; testele bune vă permit:

  • Calitatea vă asigură întreaga aplicație cu cel mai mic efort uman
  • Identificați erorile mai ușor, deoarece știți exact unde se rupe codul dvs. de la eșecurile testelor
  • Creați documentație automată pentru codul dvs
  • Evitați „codificarea constipației”, care, potrivit unui tip de pe Stack Overflow, este un mod plin de umor de a spune: „când nu știi ce să scrii în continuare sau ai o sarcină descurajantă în fața ta, începe prin a scrie mic. .”

Aș putea continua și mai departe despre cât de grozave sunt testele și cum au schimbat lumea și yada yada yada, dar înțelegeți ideea. Conceptual, testele sunt minunate.

Înrudit: Teste unitare, Cum se scrie un cod testabil și de ce este important

Teste în lumea reală

Deși există merite pentru toate cele trei tipuri de testare, acestea nu sunt scrise în majoritatea proiectelor. De ce? Ei bine, lasă-mă să o descompun:

Timp/termene limită

Toată lumea are termene limită, iar scrierea de noi teste poate împiedica îndeplinirea unuia. Poate dura timp și jumătate (sau mai mult) pentru a scrie o aplicație și testele respective. Acum, unii dintre voi nu sunteți de acord cu acest lucru, invocând în cele din urmă timpul economisit, dar nu cred că acesta este cazul și voi explica de ce în „Diferența de opinie”.

Probleme cu clienții

Adesea, clientul nu prea înțelege ce este testarea sau de ce are valoare pentru aplicație. Clienții tind să fie mai preocupați de livrarea rapidă a produselor și, prin urmare, văd testarea programatică ca fiind contraproductivă.

Sau, poate fi la fel de simplu ca clientul să nu aibă bugetul necesar pentru a plăti timpul suplimentar necesar implementării acestor teste.

Lipsa de cunostinte

Există un trib considerabil de dezvoltatori în lumea reală care nu știe că există testarea. La fiecare conferință, întâlnire, concert (chiar și în visele mele), întâlnesc dezvoltatori care nu știu să scrie teste, nu știu ce să testeze, nu știu cum să configureze cadrul pentru testare și așadar pe. Testarea nu este tocmai predată în școli și poate fi o bătaie de cap să configurați/învățați cadrul pentru a le pune în funcțiune. Deci da, există o barieră sigură la intrare.

„Este multă muncă”

Testele de scriere poate fi copleșitoare atât pentru programatorii noi, cât și pentru cei experimentați, chiar și pentru acele geni care schimbă lumea și, în plus, scrierea de teste nu este interesantă. Cineva s-ar putea gândi: „De ce ar trebui să mă angajez într-o muncă neexcitantă când aș putea implementa o caracteristică majoră cu rezultate care să-mi impresioneze clientul?” Este un argument dur.

Nu în ultimul rând, este greu să scrii teste și studenții de la informatică nu sunt pregătiți pentru asta.

A, și refactorizarea cu teste unitare nu este distractivă.

Diferența de opinie

În opinia mea, testarea unitară are sens pentru logica algoritmică, dar nu atât pentru coordonarea codului viu.

Oamenii susțin că, deși investești timp suplimentar în scrierea testelor, te economisește ore mai târziu atunci când depanezi sau schimbi codul. Vă rog să fiu diferit și să ofer o întrebare: codul dvs. este static sau se schimbă vreodată?

Pentru cei mai mulți dintre noi, este în continuă schimbare. Dacă scrii un software de succes, adaugi mereu funcții, le schimbi pe cele existente, le elimini, le consumi, orice altceva și, pentru a face față acestor modificări, trebuie să continui să-ți schimbi testele, iar schimbarea testelor necesită timp.

Dar, aveți nevoie de un fel de testare

Nimeni nu va argumenta că lipsa oricărui fel de testare este cel mai rău caz posibil. După ce faceți modificări în codul dvs., trebuie să confirmați că acesta funcționează cu adevărat. Mulți programatori încearcă să testeze manual elementele de bază: pagina este redată în browser? Se depune formularul? Este afișat conținutul corect? Și așa mai departe, dar în opinia mea, acest lucru este barbar, ineficient și necesită multă muncă.

Ce folosesc in schimb

Scopul testării unei aplicații web, fie ea manuală sau automată, este de a confirma că orice pagină dată este redată în browserul utilizatorului fără erori fatale și că își arată conținutul corect. O modalitate (și, în majoritatea cazurilor, o modalitate mai ușoară) de a realiza acest lucru este prin trimiterea de solicitări HTTP către punctele finale ale aplicației și analizarea răspunsului. Codul de răspuns vă spune dacă pagina a fost livrată cu succes. Este ușor să testați conținutul analizând corpul răspunsului cererii HTTP și căutând anumite potriviri ale șirurilor de text sau puteți fi cu un pas mai îndrăzneț și folosiți biblioteci web scraping, cum ar fi nokogiri.

Dacă unele puncte finale necesită autentificarea utilizatorului, puteți utiliza biblioteci concepute pentru automatizarea interacțiunilor (ideal atunci când faceți teste de integrare), cum ar fi mecanize pentru a vă autentifica sau faceți clic pe anumite link-uri. Într-adevăr, în imaginea de ansamblu a testării automate, aceasta seamănă mult cu integrarea sau testarea funcțională (în funcție de modul în care le utilizați), dar este mult mai rapid de scris și poate fi inclus într-un proiect existent sau adăugat la unul nou , cu mai puțin efort decât configurarea întregului cadru de testare. Absolut corect!

Înrudit: Angajați cei mai buni 3% dintre inginerii independenți QA.

Cazurile Edge prezintă o altă problemă atunci când se ocupă cu baze de date mari cu o gamă largă de valori; testarea dacă aplicația noastră funcționează fără probleme în toate seturile de date anticipate poate fi descurajantă.

O modalitate de a proceda este să anticipați toate cazurile marginale (ceea ce nu este doar dificil, este adesea imposibil) și să scrieți un test pentru fiecare. Acest lucru ar putea deveni cu ușurință sute de linii de cod (imaginați-vă groaza) și greoi de întreținut. Cu toate acestea, cu solicitări HTTP și doar o singură linie de cod, puteți testa astfel de cazuri marginale direct pe datele din producție, descărcate local pe mașina dvs. de dezvoltare sau pe un server de staging.

Acum, desigur, această tehnică de testare nu este un glonț de argint și are o mulțime de neajunsuri, la fel ca orice altă metodă, dar găsesc aceste tipuri de teste mai rapid și mai ușor de scris și modificat.

În practică: testarea cu solicitări HTTP

Deoarece am stabilit deja că scrierea codului fără nici un fel de teste însoțitoare nu este o idee bună, testul meu de bază pentru o întreagă aplicație este să trimiți cereri HTTP către toate paginile sale la nivel local și să analizez anteturile de răspuns pentru un 200 (sau dorit).

De exemplu, dacă ar fi să scriem testele de mai sus (cele care caută conținut specific și o eroare fatală) cu o solicitare HTTP (în Ruby), ar fi cam așa:

 # 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) } acoperă o mulțime de cazuri de testare; orice metodă care generează o eroare pe pagina articolului va fi surprinsă aici, astfel încât să acopere efectiv sute de linii de cod într-un singur test.

A doua parte, care prinde în mod specific eroarea de conținut, poate fi folosită de mai multe ori pentru a verifica conținutul unei pagini. (Solicitările mai complexe pot fi gestionate folosind mechanize , dar asta depășește scopul acestui blog.)

Acum, în cazurile în care doriți să testați dacă o anumită pagină funcționează pe un set mare și variat de valori ale bazei de date (de exemplu, șablonul de pagină de articol funcționează pentru toate articolele din baza de date de producție), puteți face:

 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

Aceasta va returna o serie de ID-uri ale tuturor articolelor din baza de date care nu au fost redate, așa că acum puteți merge manual la pagina articolului specific și puteți verifica problema.

Acum, înțeleg că acest mod de testare ar putea să nu funcționeze în anumite cazuri, cum ar fi testarea unui script independent sau trimiterea unui e-mail și este incontestabil mai lent decât testele unitare, deoarece facem apeluri directe către un punct final pentru fiecare test, dar când nu poți avea teste unitare, sau teste funcționale, sau ambele, asta e mai bine decât nimic.

Cum ați proceda pentru a structura aceste teste? Cu proiectele mici, necomplexe, puteți scrie toate testele într-un singur fișier și puteți rula acel fișier de fiecare dată înainte de a efectua modificările, dar majoritatea proiectelor vor necesita o suită de teste.

De obicei, scriu două până la trei teste per punct final, în funcție de ceea ce testez. De asemenea, puteți încerca să testați conținut individual (similar cu testarea unitară), dar cred că ar fi redundant și lent, deoarece veți efectua un apel HTTP pentru fiecare unitate. Dar, pe de altă parte, vor fi mai curate și ușor de înțeles.

Vă recomand să vă puneți testele în folderul obișnuit de testare, fiecare punct final principal având propriul fișier (în Rails, de exemplu, fiecare model/controler ar avea câte un fișier fiecare), iar acest fișier poate fi împărțit în trei părți în funcție de ceea ce am se testează. Am adesea cel puțin trei teste:

Testul Unu

Verificați dacă pagina revine fără erori fatale.

Testul unu verifică dacă pagina revine fără erori fatale.

Observați cum am făcut o listă cu toate punctele finale pentru Post și am repetat peste ea pentru a verifica dacă fiecare pagină este redată fără nicio eroare. Presupunând că totul a mers bine și că toate paginile au fost redate, veți vedea ceva de genul acesta în terminal: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

Dacă nu este redată nicio pagină, veți vedea ceva de genul acesta (în acest exemplu, pagina de posts/index page are eroare și, prin urmare, nu este redată): ➜ 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”}]

Testul doi

Confirmați că tot conținutul așteptat este acolo:

Testul doi confirmă că tot conținutul așteptat este acolo.

Dacă tot conținutul pe care îl așteptăm se găsește pe pagină, rezultatul arată astfel (în acest exemplu ne asigurăm că posts/:id are un titlu de postare, o descriere și o stare): ➜ 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 -- []

Dacă nu se găsește conținut așteptat pe pagină (aici ne așteptăm ca pagina să arate starea postării - „Activ” dacă postarea este activă, „Dezactivată” dacă postarea este dezactivată) rezultatul arată astfel: ➜ 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”]

Testul Trei

Verificați dacă pagina este redată în toate seturile de date (dacă există):

Testul 3 verifică dacă pagina este redată în toate seturile de date.

Dacă toate paginile sunt redate fără nicio eroare, vom obține o listă goală: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

Dacă conținutul unora dintre înregistrări are o problemă de redare (în acest exemplu, paginile cu ID-ul 2 și 5 dau o eroare), rezultatul arată astfel: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

Dacă doriți să vă jucați cu codul demonstrativ de mai sus, iată proiectul meu github.

Deci, care este mai bun? Depinde…

Testarea solicitărilor HTTP ar putea fi cel mai bun pariu dacă:

  • Lucrezi cu o aplicație web
  • Te afli într-o criză de timp și vrei să scrii ceva rapid
  • Lucrezi cu un proiect mare, un proiect preexistent în care testele nu au fost scrise, dar vrei totuși o modalitate de a verifica codul
  • Codul dvs. implică o solicitare simplă și un răspuns
  • Nu vrei să-ți petreci o mare parte din timp întreținând teste (am citit undeva test unitar = iadul de întreținere și sunt parțial de acord cu el/ea)
  • Doriți să testați dacă o aplicație funcționează cu toate valorile dintr-o bază de date existentă

Testarea tradițională este ideală atunci când:

  • Aveți de-a face cu altceva decât cu o aplicație web, cum ar fi scripturile
  • Scrii cod complex, algoritmic
  • Ai timp și buget de dedicat redactării testelor
  • Afacerea necesită fără erori sau o rată scăzută de eroare (finanțare, bază mare de utilizatori)

Mulțumesc că ai citit articolul; ar trebui să aveți acum o metodă de testare pe care o puteți utiliza implicit, una pe care vă puteți baza atunci când aveți nevoie de timp.

Legate de:
  • Performanță și eficiență: Lucrul cu HTTP/3
  • Păstrați-l criptat, păstrați-l în siguranță: Lucrul cu ESNI, DoH și DoT