Il test delle unità è una metodologia in cui le unità di codice sono testate in isolamento dal resto dell’applicazione. Un test unitario potrebbe testare una particolare funzione, oggetto, classe o modulo. I test unitari sono ottimi per sapere se le singole parti di un’applicazione funzionano o meno. La NASA deve sapere se uno scudo termico funziona o meno prima di lanciare il razzo nello spazio.
Ma i test di unità non testano se le unità funzionano insieme quando sono composte per formare un’intera applicazione. Per questo, avete bisogno di test di integrazione, che possono essere test di collaborazione tra due o più unità, o test funzionali completi end-to-end dell’intera applicazione in esecuzione (aka test di sistema). Alla fine, è necessario lanciare il razzo e vedere cosa succede quando tutte le parti sono messe insieme.
Ci sono diverse scuole di pensiero quando si tratta di test di sistema, tra cui Behavior Driven Development (BDD), e test funzionali.
Behavior Driven Development (BDD) è un ramo del Test Driven Development (TDD). BDD usa descrizioni leggibili dall’uomo dei requisiti dell’utente del software come base per i test del software. Come il Domain Driven Design (DDD), un primo passo nel BDD è la definizione di un vocabolario condiviso tra le parti interessate, gli esperti di dominio e gli ingegneri. Questo processo implica la definizione di entità, eventi e output che interessano agli utenti, e dare loro dei nomi su cui tutti possono essere d’accordo.
I professionisti del BDD usano poi questo vocabolario per creare un linguaggio specifico del dominio che possono usare per codificare i test di sistema come gli User Acceptance Test (UAT).
Ogni test è basato su una user story scritta in un linguaggio ubiquo formalmente specificato basato sull’inglese. (Un linguaggio ubiquo è un vocabolario condiviso da tutte le parti interessate.)
Un test per un trasferimento in un portafoglio di criptovalute potrebbe assomigliare a questo:
Story: Transfers change balancesAs a wallet user
In order to send money
I want wallet balances to updateGiven that I have $40 in my balance
And my friend has $10 is their balance
When I transfer $20 to my friend
Then I should have $20 in my balance
And my friend should have $30 in their balance.
Nota che questo linguaggio è focalizzato esclusivamente sul valore commerciale che un cliente dovrebbe ottenere dal software piuttosto che descrivere l’interfaccia utente del software, o come il software dovrebbe realizzare gli obiettivi. Questo è il tipo di linguaggio che potreste usare come input per il processo di design UX. Progettare questo tipo di requisiti utente in anticipo può risparmiare un sacco di lavoro più avanti nel processo, aiutando il team e i clienti a mettersi d’accordo sul prodotto che si sta costruendo.
Da questo stadio, ci sono due strade che si possono percorrere:
- Dare al test un significato tecnico concreto trasformando la descrizione in un linguaggio specifico del dominio (DSL) in modo che la descrizione leggibile dall’uomo sia anche codice leggibile dalla macchina, (continuare sulla strada del BDD) o
- Tradurre le user stories in test automatici in un linguaggio generico, come JavaScript, Rust o Haskell.
In entrambi i casi, è generalmente una buona idea trattare i vostri test come test a scatola nera, il che significa che il codice di test non dovrebbe preoccuparsi dei dettagli di implementazione della caratteristica che state testando. I test in scatola nera sono meno fragili dei test in scatola bianca perché, a differenza dei test in scatola bianca, i test in scatola nera non saranno accoppiati ai dettagli dell’implementazione, che probabilmente cambieranno quando i requisiti verranno aggiunti o aggiustati, o il codice verrà rifatto.
I sostenitori del BDD usano strumenti personalizzati come Cucumber per creare e mantenere i loro DSL personalizzati.
Per contrasto, i sostenitori dei test funzionali generalmente testano la funzionalità simulando le interazioni dell’utente con l’interfaccia e confrontando l’output effettivo con quello atteso. Nel software web, questo significa tipicamente usare un framework di test che si interfaccia con il browser web per simulare la digitazione, la pressione dei pulsanti, lo scorrimento, lo zoom, il trascinamento, ecc, e poi selezionare l’output dalla visualizzazione.
Io traduco tipicamente i requisiti utente in test funzionali piuttosto che tenere i test BDD, soprattutto a causa della complessità di integrare i framework BDD con le applicazioni moderne, e il costo di mantenere DSL personalizzati, in inglese, le cui definizioni possono finire per abbracciare diversi sistemi, e anche diversi linguaggi di implementazione.
Trovo che il DSL leggibile da un profano sia utile per specifiche di altissimo livello come strumento di comunicazione tra le parti interessate, ma un tipico sistema software richiederà ordini di grandezza in più di test di basso livello per produrre un codice adeguato e una copertura dei casi che impedisca ai bug più evidenti di raggiungere la produzione.
In pratica, dovete tradurre “trasferisco 20 dollari al mio amico” in qualcosa come:
- Aprire il portafoglio
- Cliccare sul trasferimento
- compilare l’importo
- compilare l’indirizzo del portafoglio del destinatario
- Cliccare
- Aspetta una finestra di dialogo di conferma
- Clicca “Conferma la transazione”
Un livello sottostante, si sta mantenendo lo stato per il flusso di lavoro “trasferimento di denaro”, e vorrete dei test unitari che assicurino che l’importo corretto venga trasferito all’indirizzo corretto del portafoglio, e un livello più in basso, vorrete colpire le API della blockchain per assicurare che i saldi del portafoglio siano stati effettivamente regolati in modo appropriato (qualcosa che il client potrebbe anche non avere una vista).
Queste diverse esigenze di test sono meglio servite da diversi livelli di test:
- I test unitari possono testare che lo stato locale del client sia aggiornato correttamente e presentato correttamente nella vista del client.
- I test funzionali possono testare le interazioni dell’UI e assicurare che i requisiti dell’utente siano soddisfatti al livello dell’UI. Questo assicura anche che gli elementi dell’UI siano cablati in modo appropriato.
- I test di integrazione possono verificare che le comunicazioni API avvengano in modo appropriato e che gli importi del portafoglio dell’utente siano stati effettivamente aggiornati correttamente sulla blockchain.
Non ho mai incontrato uno stakeholder profano che sia lontanamente consapevole di tutti i test funzionali che verificano anche il comportamento dell’UI di livello superiore, figuriamoci uno che si preoccupa di tutti i comportamenti di livello inferiore. Dato che i profani non sono interessati, perché pagare il costo di mantenere un DSL per tradurre per loro?
A prescindere dal fatto che si pratichi o meno il processo BDD completo, ha un sacco di grandi idee e pratiche che non dovremmo perdere di vista. In particolare:
- La formazione di un vocabolario condiviso che gli ingegneri e le parti interessate possono usare per comunicare efficacemente sulle esigenze degli utenti e sulle soluzioni software.
- La creazione di storie utente e scenari che aiutano a formulare criteri di accettazione e una definizione di fatto per una particolare caratteristica del software.
- La pratica della collaborazione tra gli utenti, il team della qualità, il team del prodotto e gli ingegneri per raggiungere il consenso su ciò che il team sta costruendo.
Un altro approccio al test di sistema è il test funzionale.
Che cos’è il test funzionale?
Il termine “test funzionale” può confondere perché ha avuto diversi significati nella letteratura sul software.
IEEE 24765 dà due definizioni:
1. test che ignora il meccanismo interno di un sistema o componente e si concentra esclusivamente sugli output generati in risposta a determinati input e condizioni di esecuzione
2. test condotto per valutare la conformità di un sistema o di un componente ai requisiti funzionali specificati
La prima definizione è abbastanza generale da applicarsi a quasi tutte le forme popolari di test, e ha già un nome perfettamente adatto e ben compreso dai tester di software: “black box testing”. Quando parlerò di black box testing, userò questo termine, invece.
La seconda definizione è di solito usata in contrasto con i test che non sono direttamente legati alle caratteristiche e alle funzionalità dell’app, ma si concentrano invece su altre caratteristiche dell’app, come i tempi di carico, i tempi di risposta dell’UI, i test di carico del server, i test di penetrazione della sicurezza, e così via. Di nuovo, questa definizione è troppo vaga per essere molto utile da sola. Di solito, vogliamo essere più specifici sul tipo di test che stiamo facendo, ad esempio, unit testing, smoke testing, user acceptance testing?
Per queste ragioni, preferisco un’altra definizione che è stata popolare recentemente. IBM’s Developer Works dice:
I test funzionali sono scritti dalla prospettiva dell’utente e si concentrano sul comportamento del sistema che interessa agli utenti.
Questo è molto più vicino al punto, ma se abbiamo intenzione di automatizzare i test, e questi test devono testare dal punto di vista dell’utente, ciò significa che avremo bisogno di scrivere test che interagiscono con l’UI.
Tali test possono anche essere chiamati “UI testing” o “E2E testing”, ma questi nomi non sostituiscono il bisogno del termine “test funzionali” perché c’è una classe di test UI che testa cose come stili e colori, che non sono direttamente collegati ai requisiti utente come “dovrei essere in grado di trasferire denaro al mio amico”.
Usare “test funzionali” per riferirsi al test dell’interfaccia utente per assicurarsi che soddisfi i requisiti utente specificati è di solito usato in contrasto con il test delle unità, che è definito come:
il test di singole unità di codice (come funzioni o moduli) in isolamento dal resto dell’applicazione
In altre parole, mentre un test unitario serve a testare singole unità di codice (funzioni, oggetti, classi, moduli) in isolamento dall’applicazione, un test funzionale serve a testare le unità in integrazione con il resto dell’applicazione, dal punto di vista dell’utente che interagisce con la UI.
Mi piace la classificazione di “test delle unità” per le unità di codice dal punto di vista dello sviluppatore, e “test funzionali” per i test dell’interfaccia utente dal punto di vista dell’utente.
Test delle unità vs test funzionali
I test delle unità sono tipicamente scritti dal programmatore che le implementa, e testano dalla prospettiva del programmatore.
I test funzionali sono informati dai criteri di accettazione dell’utente e dovrebbero testare l’applicazione dalla prospettiva dell’utente per assicurare che i requisiti dell’utente siano soddisfatti. In molti team, i test funzionali possono essere scritti o ampliati dagli ingegneri della qualità, ma ogni ingegnere del software dovrebbe essere consapevole di come vengono scritti i test funzionali per il progetto, e quali test funzionali sono richiesti per completare la “definizione di fatto” per un particolare set di funzionalità.
I test di unità sono scritti per testare singole unità in isolamento dal resto del codice. Ci sono due benefici principali in questo approccio:
- I test di unità vengono eseguiti molto velocemente perché non dipendono da altre parti del sistema, e come tali, tipicamente non hanno I/O asincroni da aspettare. È molto più veloce e meno costoso trovare e correggere un difetto con i test unitari che aspettare l’esecuzione di una suite di integrazione completa. I test unitari tipicamente si completano in millisecondi, invece che in minuti o ore.
- Le unità devono essere modulari per rendere facile testarle in isolamento dalle altre unità. Questo ha l’ulteriore vantaggio di essere molto buono per l’architettura dell’applicazione. Il codice modulare è più facile da estendere, mantenere o sostituire perché gli effetti del suo cambiamento sono generalmente limitati all’unità del modulo sotto test. Le applicazioni modulari sono più flessibili e più facili da lavorare per gli sviluppatori nel tempo.
I test funzionali d’altra parte:
- Prendono più tempo per essere eseguiti, perché devono testare il sistema end-to-end, integrandosi con tutte le varie parti e sottosistemi su cui l’applicazione si basa per consentire il flusso di lavoro dell’utente che viene testato. Le grandi suite di integrazione a volte richiedono ore per essere eseguite. Ho sentito storie di suite di integrazione che hanno richiesto giorni per essere eseguite. Raccomando di iper-ottimizzare la pipeline di integrazione per eseguirla in parallelo in modo che possa essere completata in meno di 10 minuti – ma questo è ancora troppo tempo per gli sviluppatori per aspettare ogni cambiamento.
- Assicurarsi che le unità lavorino insieme come un intero sistema. Anche se avete un’eccellente copertura del codice di test delle unità, avete ancora bisogno di testare le vostre unità integrate con il resto dell’applicazione. Non importa se gli scudi termici della NASA funzionano se non rimangono attaccati al razzo al rientro. I test funzionali sono una forma di test di sistema che assicurano che il sistema nel suo complesso si comporti come previsto quando è completamente integrato.
I test funzionali senza test delle unità non possono mai fornire una copertura del codice abbastanza profonda per essere sicuri di avere un’adeguata rete di sicurezza di regressione per la consegna continua. I test unitari forniscono la profondità della copertura del codice. I test funzionali forniscono l’ampiezza della copertura dei casi di test dei requisiti utente.
I test funzionali ci aiutano a costruire il prodotto giusto. (Validazione)
I test unitari ci aiutano a costruire il prodotto giusto. (Verifica)
Sono necessari entrambi.
Nota: Vedere Validazione vs Verifica. La distinzione Build the right product vs build the product right è stata succintamente descritta da Barry Boehm.
Come scrivere test funzionali per applicazioni web
Ci sono molti framework che permettono di creare test funzionali per applicazioni web. Molti di loro usano un’interfaccia chiamata Selenium. Selenium è una soluzione di automazione multipiattaforma e cross-browser creata nel 2004 che permette di automatizzare le interazioni con il browser web. Il problema con Selenium è che è un motore esterno ai browser che si basa su Java, e farlo funzionare insieme ai vostri browser può essere più difficile del necessario.
Di recente, è spuntata una nuova famiglia di prodotti che si integrano molto più facilmente con i browser con meno pezzi da preoccuparsi di installare e configurare. Una di queste soluzioni si chiama TestCafe. È quella che attualmente uso e raccomando.
Scriviamo un test funzionale per il sito web TDD Day. Per prima cosa, vorrete creare un progetto per esso. In un terminale:
mkdir tddday
cd tddday
npm init -y # initialize a package.json
npm install --save-dev testcafe
Ora dobbiamo aggiungere uno script "testui"
al nostro package.json
nel blocco scripts
:
{
"scripts": {
"testui": "testcafe chrome src/functional-tests/"
}
// other stuff...
}
Puoi eseguire i test digitando npm run testui
, ma non ci sono ancora test da eseguire.
Crea un nuovo file a src/functional-tests/index-test.js
:
import { Selector } from 'testcafe';
TestCafe rende automaticamente disponibili le funzioni fixture
e test
. Si può usare fixture
con la sintassi tagged template literal per creare titoli per gruppi di test:
fixture `TDD Day Homepage`
.page('https://tddday.com');
Ora si può selezionare dalla pagina e fare asserzioni usando le funzioni test
e Select
. Quando si mette tutto insieme, appare così:
TestCafe lancerà il browser Chrome, caricherà la pagina, aspetterà che la pagina si carichi e aspetterà che il vostro selettore corrisponda a una selezione. Se non corrisponde a nulla, il test finirà per scadere e fallire. Se corrisponde a qualcosa, controllerà l’effettivo valore selezionato rispetto al valore atteso, e il test fallirà se non corrispondono.
TestCafe fornisce metodi per testare tutti i tipi di interazioni dell’interfaccia utente, inclusi il click, il trascinamento, la digitazione del testo, e così via.
TestCafe fornisce anche una ricca API di selezione per rendere le selezioni DOM indolori.
Testiamo il pulsante di registrazione per assicurarci che navighi nella pagina corretta al click. Per prima cosa, abbiamo bisogno di un modo per controllare la posizione attuale della pagina. Il nostro codice TestCafe è in esecuzione in Node, ma abbiamo bisogno che venga eseguito nel client. TestCafe ci fornisce un modo per eseguire il codice nel client. Per prima cosa, dobbiamo aggiungere ClientFunction
alla nostra linea di importazione:
import { Selector, ClientFunction } from 'testcafe';
Ora possiamo usarlo per testare la posizione della finestra:
Se non sei sicuro di come fare quello che stai cercando di fare, TestCafe Studio ti permette di registrare e riprodurre i test. TestCafe Studio è un IDE visivo per registrare e modificare interattivamente i test funzionali. È progettato in modo che un ingegnere di test che potrebbe non conoscere JavaScript possa costruire una suite di test funzionali. I test che genera attendono automaticamente lavori asincroni come il caricamento delle pagine. Come il motore TestCafe, TestCafe Studio può produrre test che possono essere eseguiti simultaneamente su molti browser, e anche su dispositivi remoti.
TestCafe Studio è un prodotto commerciale con una prova gratuita. Non è necessario acquistare TestCafe Studio per utilizzare il motore open source TestCafe, ma l’editor visuale con funzioni di registrazione integrate è sicuramente uno strumento che vale la pena di esplorare per vedere se è giusto per il vostro team.
TestCafe ha stabilito un nuovo standard per i test funzionali cross-browser. Avendo sopportato molti anni di tentativi di automatizzare i test multipiattaforma, sono felice di dire che c’è finalmente un modo abbastanza indolore per creare test funzionali, e ora non c’è una buona scusa per trascurare i vostri test funzionali, anche se non avete ingegneri della qualità dedicati che vi aiutino a costruire la vostra suite di test funzionali.
Dos and Don’ts of Functional Tests
- Non alterate il DOM. Se lo fai, il tuo test runner (ad esempio TestCafe) potrebbe non essere in grado di capire come è cambiato il DOM, e l’alterazione del DOM potrebbe avere un impatto sulle altre asserzioni che potrebbero basarsi sull’output del DOM.
- Non condividere lo stato mutabile tra i test. Poiché sono così lenti, è incredibilmente importante che i test funzionali possano essere eseguiti in parallelo, e non possono farlo in modo deterministico se sono in competizione per lo stesso stato mutabile condiviso, il che potrebbe causare nondeterminismo a causa di condizioni di gara. Poiché state eseguendo test di sistema, tenete a mente che se state modificando i dati utente, dovreste avere diversi dati utente di test nel database per i diversi test in modo che non falliscano casualmente a causa di condizioni di gara.
- Non mischiate i test funzionali con i test unitari. I test unitari e quelli funzionali dovrebbero essere scritti da prospettive diverse, ed eseguiti in momenti diversi. I test unitari dovrebbero essere scritti dalla prospettiva dello sviluppatore ed essere eseguiti ogni volta che lo sviluppatore fa un cambiamento, e dovrebbero essere completati in meno di 3 secondi. I test funzionali dovrebbero essere scritti dal punto di vista dell’utente, e coinvolgono I/O asincroni che possono rendere l’esecuzione dei test troppo lenta per un feedback immediato dello sviluppatore su ogni cambiamento di codice. Dovrebbe essere facile eseguire i test unitari senza innescare l’esecuzione dei test funzionali.
- Eseguite i test in modalità headless, se potete, il che significa che l’interfaccia utente del browser non ha effettivamente bisogno di essere lanciata, e i test possono essere eseguiti più velocemente. La modalità headless è un ottimo modo per accelerare la maggior parte dei test funzionali, ma c’è un piccolo sottoinsieme di test che non può essere eseguito in modalità headless, semplicemente perché la funzionalità su cui si basano non funziona in modalità headless. Alcune pipeline CI/CD richiederanno di eseguire i test funzionali in modalità headless, quindi se avete alcuni test che non possono essere eseguiti in modalità headless, potrebbe essere necessario escluderli dall’esecuzione CI/CD. Assicuratevi che il team di qualità sia all’erta per questo scenario.
- Eseguite i test su più dispositivi. I vostri test passano ancora sui dispositivi mobili? TestCafe può essere eseguito su browser remoti senza installare TestCafe sui dispositivi remoti. Tuttavia, la funzionalità di screenshot non funziona sui browser remoti.
- Cattura gli screenshot sui fallimenti dei test. Può essere utile fare uno screenshot se i tuoi test falliscono per aiutare a diagnosticare cosa è andato storto. TestCafe studio ha un’opzione di configurazione dell’esecuzione per questo.
- Tieni i tuoi test funzionali sotto i 10 minuti. Qualsiasi tempo in più creerà un ritardo eccessivo tra lo sviluppatore che lavora su una funzione e la correzione di qualcosa che è andato storto. 10 minuti sono abbastanza lunghi per uno sviluppatore per essere occupato a lavorare sulla caratteristica successiva, e se il test fallisce dopo più di 10 minuti, probabilmente interromperà lo sviluppatore che è passato al compito successivo. Un compito interrotto richiede in media il doppio del tempo per essere completato e contiene circa il doppio degli errori. TestCafe permette di eseguire molti test contemporaneamente, e l’opzione del browser remoto può farlo attraverso una flotta di server di test. Raccomando di approfittare di queste caratteristiche per mantenere la durata dei test il più breve possibile.
- Fermare la pipeline di consegna continua quando i test falliscono. Uno dei grandi vantaggi dei test automatizzati è la capacità di proteggere i vostri clienti dalle regressioni – bug nelle caratteristiche che funzionavano. Questo processo di rete di sicurezza può essere automatizzato in modo da avere buona fiducia che il vostro rilascio sia relativamente privo di bug. I test nella pipeline CI/CD eliminano efficacemente la paura del cambiamento di un team di sviluppo, che può essere un serio freno alla produttività degli sviluppatori.
Passi successivi
Entra in TDD Day.com – un curriculum TDD di tutta la giornata con 5 ore di contenuti video registrati, progetti per imparare i test unitari e funzionali, come testare i componenti React, e un quiz interattivo per essere sicuri di aver imparato il materiale.