Test delle richieste HTTP: uno strumento di sopravvivenza per sviluppatori
Pubblicato: 2022-03-11Cosa fare quando una suite di test non è fattibile
Ci sono volte in cui noi – programmatori e/o nostri clienti – abbiamo risorse limitate con cui scrivere sia il deliverable atteso che i test automatizzati per quel deliverable. Quando l'applicazione è abbastanza piccola, puoi tagliare gli angoli e saltare i test perché ricordi (principalmente) cosa succede altrove nel codice quando aggiungi una funzionalità, correggi un bug o refactoring. Detto questo, non lavoreremo sempre con piccole applicazioni, inoltre, tendono a diventare più grandi e complesse nel tempo. Questo rende i test manuali difficili e super fastidiosi.
Per i miei ultimi progetti, sono stato costretto a lavorare senza test automatizzati e, onestamente, è stato imbarazzante ricevere un'e-mail dal cliente dopo un push del codice per dire che l'applicazione si stava rompendo in punti in cui non avevo nemmeno toccato il codice.
Quindi, nei casi in cui il mio cliente non avesse budget o intenzione di aggiungere alcun framework di test automatizzato, ho iniziato a testare le funzionalità di base dell'intero sito Web inviando una richiesta HTTP a ogni singola pagina, analizzando le intestazioni di risposta e cercando il "200" risposta. Sembra chiaro e semplice, ma c'è molto che puoi fare per garantire la fedeltà senza dover effettivamente scrivere test, unità, funzionalità o integrazione.
Test automatizzati
Nello sviluppo web, i test automatizzati comprendono tre tipi principali di test: test unitari, test funzionali e test di integrazione. Spesso combiniamo unit test con test funzionali e di integrazione per assicurarci che tutto funzioni senza intoppi nell'intera applicazione. Quando questi test vengono eseguiti all'unisono o in sequenza (preferibilmente con un singolo comando o clic), iniziamo a chiamarli test automatici, unitari o meno.
In gran parte lo scopo di questi test (almeno nello sviluppo web) è assicurarsi che tutte le pagine dell'applicazione vengano visualizzate senza problemi, senza errori o bug fatali (arresto dell'applicazione).
Test unitario
Il test unitario è un processo di sviluppo software in cui le parti più piccole del codice, le unità, vengono testate in modo indipendente per verificarne il corretto funzionamento. Ecco un esempio 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
Prove funzionali
Il test funzionale è una tecnica utilizzata per verificare le caratteristiche e la funzionalità del sistema o del software, progettata per coprire tutti gli scenari di interazione dell'utente, inclusi i percorsi di errore e i casi limite.
Nota: tutti i nostri esempi sono in Ruby.
test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end
Test d'integrazione
Una volta che i moduli sono stati testati per unità, vengono integrati uno ad uno, in sequenza, per verificare il comportamento combinatorio e per convalidare che i requisiti siano implementati correttamente.
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
Test in un mondo ideale
I test sono ampiamente accettati nel settore e hanno senso; buoni test ti permettono di:
- La qualità assicura l'intera applicazione con il minimo sforzo umano
- Identifica i bug più facilmente perché sai esattamente dove si sta interrompendo il tuo codice a causa di errori di test
- Crea documentazione automatica per il tuo codice
- Evita la "stitichezza del codice", che, secondo un tizio su Stack Overflow, è un modo umoristico per dire "quando non sai cosa scrivere dopo, o hai un compito arduo di fronte a te, inizia scrivendo in piccolo .”
Potrei continuare all'infinito su quanto siano fantastici i test e su come hanno cambiato il mondo e yada yada yada, ma hai capito. Concettualmente, i test sono fantastici.
Test nel mondo reale
Sebbene ci siano dei meriti in tutti e tre i tipi di test, non vengono scritti nella maggior parte dei progetti. Come mai? Bene, lascia che lo scomponga:
Tempo/Scadenze
Ognuno ha delle scadenze e scrivere nuovi test può intralciare il raggiungimento di uno. La scrittura di un'applicazione e dei relativi test può richiedere tempo e mezzo (o più). Ora, alcuni di voi non sono d'accordo con questo, citando il tempo risparmiato alla fine, ma non credo che sia così e spiegherò il perché in "Differenza di opinione".
Problemi del cliente
Spesso, il client non capisce veramente cosa sia il test o perché abbia valore per l'applicazione. I clienti tendono ad essere più interessati alla consegna rapida dei prodotti e quindi considerano i test programmatici controproducenti.
Oppure, potrebbe essere semplice in quanto il cliente non ha il budget per pagare il tempo aggiuntivo necessario per implementare questi test.
Mancanza di conoscenza
C'è una considerevole tribù di sviluppatori nel mondo reale che non sa che esistono test. Ad ogni conferenza, incontro, concerto (anche nei miei sogni), incontro sviluppatori che non sanno scrivere test, non sanno cosa testare, non sanno come impostare il framework per i test, e così via su. I test non vengono insegnati esattamente nelle scuole e può essere una seccatura impostare/apprendere il framework per farli funzionare. Quindi sì, c'è una chiara barriera all'ingresso.
"È un sacco di lavoro"
Scrivere test può essere opprimente sia per i programmatori nuovi che per quelli esperti, anche per quei tipi geniali che cambiano il mondo, e per finire, scrivere test non è entusiasmante. Si potrebbe pensare: "Perché dovrei impegnarmi in un lavoro poco entusiasmante quando potrei implementare una funzionalità importante con risultati che impressioneranno il mio cliente?" È un argomento difficile.
Infine, ma non meno importante, è difficile scrivere test e gli studenti di informatica non sono formati per questo.
Oh, e il refactoring con gli unit test non è divertente.
Differenza di opinioni
Secondo me, lo unit test ha senso per la logica algoritmica ma non tanto per coordinare il codice vivente.
Le persone affermano che anche se stai investendo più tempo in anticipo nella scrittura dei test, ti fa risparmiare ore dopo il debug o la modifica del codice. Mi permetto di dissentire e pongo una domanda: il tuo codice è statico o cambia in continuazione?
Per la maggior parte di noi, è in continua evoluzione. Se stai scrivendo software di successo, aggiungi sempre funzionalità, modifichi quelle esistenti, le rimuovi, le mangi, qualunque cosa, e per accogliere queste modifiche, devi continuare a modificare i test e cambiare i test richiede tempo.
Ma hai bisogno di una sorta di test
Nessuno sosterrà che la mancanza di qualsiasi tipo di test sia il peggior caso possibile. Dopo aver apportato modifiche al codice, è necessario confermare che funzioni effettivamente. Molti programmatori provano a testare manualmente le basi: il rendering della pagina è nel browser? Il modulo viene inviato? Viene visualizzato il contenuto corretto? E così via, ma secondo me è barbaro, inefficiente e laborioso.
Cosa uso invece
Lo scopo del test di un'app Web, manuale o automatizzata, è confermare che una determinata pagina venga visualizzata nel browser dell'utente senza errori irreversibili e che mostri correttamente il suo contenuto. Un modo (e nella maggior parte dei casi, un modo più semplice) per ottenere ciò è inviare richieste HTTP agli endpoint dell'app e analizzare la risposta. Il codice di risposta ti dice se la pagina è stata consegnata correttamente. È facile testare il contenuto analizzando il corpo della risposta della richiesta HTTP e cercando corrispondenze di stringhe di testo specifiche, oppure puoi essere un passo più elaborato e utilizzare librerie di scraping web come nokogiri.
Se alcuni endpoint richiedono un accesso utente, puoi utilizzare librerie progettate per automatizzare le interazioni (ideale quando si eseguono test di integrazione) come meccanizzare per accedere o fare clic su determinati collegamenti. In realtà, nel quadro generale dei test automatici, assomiglia molto all'integrazione o al test funzionale (a seconda di come li usi), ma è molto più veloce da scrivere e può essere incluso in un progetto esistente o aggiunto a uno nuovo , con uno sforzo minore rispetto all'impostazione dell'intero framework di test. A posto!
I casi limite presentano un altro problema quando si tratta di database di grandi dimensioni con un'ampia gamma di valori; verificare se la nostra applicazione funziona senza problemi su tutti i set di dati previsti può essere scoraggiante.

Un modo per farlo è anticipare tutti i casi limite (che non è solo difficile, è spesso impossibile) e scrivere un test per ciascuno. Questo potrebbe facilmente diventare centinaia di righe di codice (immagina l'orrore) e ingombrante da mantenere. Tuttavia, con le richieste HTTP e una sola riga di codice, puoi testare tali casi limite direttamente sui dati di produzione, scaricati localmente sulla tua macchina di sviluppo o su un server di staging.
Ora, ovviamente, questa tecnica di test non è un proiettile d'argento e ha molte carenze, come qualsiasi altro metodo, ma trovo che questi tipi di test siano più veloci e facili da scrivere e modificare.
In pratica: test con richieste HTTP
Poiché abbiamo già stabilito che scrivere codice senza alcun tipo di test di accompagnamento non è una buona idea, il mio test di base per un'intera applicazione è inviare richieste HTTP a tutte le sue pagine localmente e analizzare le intestazioni di risposta per un 200
(o desiderato) codice.
Ad esempio, se dovessimo scrivere i test precedenti (quelli che cercano contenuto specifico e un errore fatale) con una richiesta HTTP invece (in Ruby), sarebbe qualcosa del genere:
# 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 linea curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }
copre molti casi di test; qualsiasi metodo che genera un errore nella pagina dell'articolo verrà catturato qui, quindi copre efficacemente centinaia di righe di codice in un test.
La seconda parte, che rileva in modo specifico l'errore di contenuto, può essere utilizzata più volte per controllare il contenuto di una pagina. (Le richieste più complesse possono essere gestite utilizzando mechanize
, ma questo va oltre lo scopo di questo blog.)
Ora, nei casi in cui desideri verificare se una pagina specifica funziona su un insieme ampio e variegato di valori del database (ad esempio, il modello di pagina dell'articolo funziona per tutti gli articoli nel database di produzione), potresti fare:
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
Questo restituirà un array di ID di tutti gli articoli nel database che non sono stati visualizzati, quindi ora puoi andare manualmente alla pagina dell'articolo specifico e controllare il problema.
Ora, capisco che questo modo di testare potrebbe non funzionare in alcuni casi, come testare uno script autonomo o inviare un'e-mail, ed è innegabilmente più lento degli unit test perché stiamo effettuando chiamate dirette a un endpoint per ogni test, ma quando non puoi avere unit test o test funzionali o entrambi, questo è meglio di niente.
Come strutturaresti questi test? Con progetti piccoli e non complessi, puoi scrivere tutti i tuoi test in un file ed eseguire quel file ogni volta prima di eseguire il commit delle modifiche, ma la maggior parte dei progetti richiederà una suite di test.
Di solito scrivo da due a tre test per endpoint, a seconda di cosa sto testando. Puoi anche provare a testare i singoli contenuti (simile allo unit test), ma penso che sarebbe ridondante e lento poiché eseguirai una chiamata HTTP per ogni unità. Ma, d'altra parte, saranno più puliti e facili da capire.
Ti consiglio di inserire i tuoi test nella tua normale cartella di test con ogni punto finale principale che ha il proprio file (in Rails, ad esempio, ogni modello/controller avrebbe un file ciascuno) e questo file può essere diviso in tre parti in base a ciò che stanno testando. Spesso ho almeno tre test:
Prova uno
Verifica che la pagina ritorni senza errori irreversibili.
Nota come ho creato un elenco di tutti gli endpoint per Post
e l'ho ripetuto per verificare che ogni pagina venga visualizzata senza errori. Supponendo che tutto sia andato bene e che tutte le pagine siano state renderizzate, vedrai qualcosa del genere nel terminale: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []
Se una pagina non viene visualizzata, vedrai qualcosa del genere (in questo esempio, la posts/index page
contiene errori e quindi non viene visualizzata): ➜ 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”}]
Prova due
Conferma che tutti i contenuti previsti sono presenti:
Se tutto il contenuto che ci aspettiamo viene trovato sulla pagina, il risultato sarà simile al seguente (in questo esempio ci assicuriamo che posts/:id
abbia un titolo, una descrizione e uno stato del post): ➜ 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 -- []
Se nella pagina non viene trovato alcun contenuto previsto (qui ci aspettiamo che la pagina mostri lo stato del post - 'Attivo' se il post è attivo, 'Disabilitato' se il post è disabilitato) il risultato sarà simile al seguente: ➜ 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”]
Prova tre
Verifica che la pagina venga visualizzata su tutti i set di dati (se presenti):
Se tutte le pagine vengono visualizzate senza errori, otterremo una lista vuota: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []
Se il contenuto di alcuni record presenta un problema di rendering (in questo esempio, le pagine con ID 2 e 5 danno un errore) il risultato è simile al seguente: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]
Se vuoi giocherellare con il codice dimostrativo sopra, ecco il mio progetto github.
Allora, qual è il migliore? Dipende…
Il test delle richieste HTTP potrebbe essere la soluzione migliore se:
- Stai lavorando con un'app web
- Sei in una crisi di tempo e vuoi scrivere qualcosa in fretta
- Stai lavorando con un grande progetto, un progetto preesistente in cui i test non sono stati scritti, ma vuoi comunque un modo per controllare il codice
- Il tuo codice prevede una semplice richiesta e risposta
- Non vuoi passare gran parte del tuo tempo a mantenere i test (ho letto da qualche parte unit test = inferno di manutenzione e sono parzialmente d'accordo con lui/lei)
- Si desidera verificare se un'applicazione funziona su tutti i valori in un database esistente
I test tradizionali sono ideali quando:
- Hai a che fare con qualcosa di diverso da un'applicazione web, come gli script
- Stai scrivendo un codice algoritmico complesso
- Hai tempo e budget da dedicare alla scrittura dei test
- L'azienda richiede un tasso di errore privo di bug o basso (finanza, ampia base di utenti)
Grazie per aver letto l'articolo; ora dovresti avere un metodo per testare che puoi utilizzare per impostazione predefinita, uno su cui puoi contare quando hai poco tempo.
- Prestazioni ed efficienza: utilizzo di HTTP/3
- Mantienilo crittografato, mantienilo al sicuro: lavorare con ESNI, DoH e DoT