Dai alla suite la forma di una piramide
Mantieni la maggior parte dei test come test unitari veloci e isolati, con proporzionalmente meno test di integrazione e solo una manciata di test end-to-end per i percorsi critici. Questo spinge ogni difetto al livello più economico in grado di coglierlo, mantenendo il feedback rapido e la suite affidabile; il "cono gelato" invertito fatto soprattutto di test end-to-end produce suite lente e instabili che i team smettono di fidarsi.
- Fai: testa una regola di prezzo come test unitario, l'SQL del repository ordini come test di integrazione e solo il checkout end-to-end.
- Non fare: verificare un caso limite di un messaggio di validazione pilotando l'intera UI in un test Selenium.
Verifica il comportamento, non l'implementazione
Scrivi i test contro il contratto osservabile del system under test - i suoi output ed effetti collaterali - anziché contro le sue chiamate e la sua struttura interne. I test orientati al comportamento sopravvivono al refactoring e documentano l'intento, mentre i test che affermano "il metodo X è stato chiamato" si rompono a ogni cambiamento interno e possono passare anche quando il codice è sbagliato.
- Fai: verifica che
transfersposti il saldo e rifiuti uno scoperto. - Non fare: verificare che
transferabbia chiamatorepository.saveesattamente due volte in un ordine specifico.
Rendi ogni test indipendente e deterministico
Ogni test dovrebbe poter girare da solo, in qualsiasi ordine e in parallelo con lo stesso risultato, senza stato mutabile condiviso né assunzioni di ordinamento. Determinismo e isolamento sono ciò che rende una suite affidabile e parallelizzabile; la loro assenza è la causa principale di fallimenti instabili e dipendenti dall'ordine.
- Fai: crea o reimposta la fixture per test e inietta un orologio fisso per la logica dipendente dal tempo.
- Non fare: lasciare che un test lasci righe in un database condiviso su cui un test successivo fa silenziosamente affidamento.
Controlla tempo, casualità e altre fonti di non determinismo
Non lasciare mai che il codice sotto test legga l'orologio, generi valori casuali o dipenda dal timing di rete reale direttamente. Inietta queste cose come dipendenze (un Clock, una fonte casuale con seed, un client HTTP fittizio) così che i test possano fissarle a valori noti e verificare il comportamento esatto ai confini.
- Fai: inietta
Clock.fixed(...)e verifica la scadenza del token al secondo esatto. - Non fare: chiamare
Instant.now()dentro il metodo e aggiungere unosleepper "aspettare" il confine.
Usa infrastruttura reale per i test di integrazione
Quando verifichi persistenza, messaggistica o serializzazione, esegui contro la tecnologia reale in un container usa-e-getta (per esempio via Testcontainers) anziché un sostituto in memoria che si comporta diversamente. I sostituti fittizi o in stile H2 nascondono difetti di dialetto, mapping e transazioni che solo il motore reale rivela.
- Fai: avvia un container PostgreSQL reale ed esegui contro di esso il test del repository.
- Non fare: testare SQL specifico di Postgres contro un database H2 in memoria e dare per scontato che si trasferisca.
Testa i percorsi infelici, non solo il percorso felice
Per ogni test del percorso felice, aggiungi casi negativi e di confine: input non validi, dati mancanti, timeout e risposte di errore. La maggior parte dei difetti in produzione e dei problemi di sicurezza vive nella gestione degli errori e nelle condizioni limite, perciò una suite che esercita solo lo scenario di default dà falsa fiducia.
- Fai: verifica che l'API restituisca 400 su input malformato e 409 su una scrittura in conflitto.
- Non fare: testare solo la richiesta ben formata e dare per scontato che i rami di errore funzionino.
Trasforma ogni correzione di bug in un test di regressione
Prima di correggere un difetto, scrivi un test che lo riproduce e fallisce; la correzione è completa quando quel test passa. Questo garantisce che lo specifico bug non possa tornare silenziosamente e fa crescere costantemente una suite di regressione focalizzata su fallimenti reali anziché immaginati.
- Fai: aggiungi un test fallito che cattura l'input esatto che ha innescato l'incidente in produzione, poi correggi.
- Non fare: applicare la patch al codice e andare avanti, senza nulla che fermi la stessa regressione il mese prossimo.
Evita l'eccesso di mock
Sostituisci con mock solo i confini che non puoi facilmente eseguire per davvero - servizi esterni, dipendenze lente o non deterministiche - e preferisci oggetti reali o fake altrove. L'eccesso di mock accoppia i test alla struttura interna delle chiamate, rende doloroso il refactoring e produce test che passano anche quando il comportamento integrato è rotto.
- Fai: usa un oggetto di dominio reale e un fake del repository in memoria in un test di servizio.
- Non fare: mockare ogni collaboratore così che il test ridescriva semplicemente l'implementazione.
Mantieni i test leggibili con una struttura chiara
Struttura ogni test come Arrange-Act-Assert (o Given-When-Then) con un solo comportamento logico per test e un nome descrittivo che indichi lo scenario e l'esito atteso. I test leggibili fungono da documentazione viva e rendono i fallimenti auto-esplicativi; un test che ha bisogno di commenti per spiegare cosa verifica di solito fa troppo.
- Fai: nomina un test
rifiuta_prelievo_se_saldo_insufficientecon tre fasi chiare. - Non fare: stipare cinque scenari non correlati in un solo
test1con un groviglio di assert.
Misura la qualità dei test oltre la copertura delle righe
Tratta la copertura come strumento per individuare codice non testato, non come obiettivo, e integrala con il mutation testing su moduli critici o complessi per confermare che i test catturino davvero i guasti. Un'alta copertura con asserzioni deboli dà un falso senso di sicurezza; un mutante sopravvissuto rivela un comportamento che nessun test verifica davvero.
- Fai: esegui il mutation testing sul modulo di fatturazione e aggiungi test per i mutanti sopravvissuti.
- Non fare: festeggiare il 95% di copertura delle righe su codice i cui test non affermano nulla di significativo.
Esegui i test giusti nella fase giusta in CI
Metti un gate a ogni commit sui test unitari (e di integrazione leggeri) veloci così che il feedback resti rapido, e pianifica le suite lente e ampie - end-to-end completo, soak e performance - con una cadenza meno frequente. Mantenere veloce il gate rapido preserva il flusso degli sviluppatori, mentre le suite più lente colgono comunque ciò che solo loro possono.
- Fai: esegui unit + integrazione a ogni push e la suite E2E e di carico completa di notte.
- Non fare: mettere una suite end-to-end di 40 minuti sul percorso di ogni singolo commit.
Valuta le funzionalità di IA e LLM con un golden set fisso
Costruisci un dataset di valutazione rappresentativo con risposte di riferimento o proprietà, assegna punteggi agli output con metriche automatiche (faithfulness, relevancy, context recall) più una revisione umana mirata e metti un gate ai rilasci su quei punteggi in CI. Mantieni fisso il golden set e fai variare il sistema, così da misurare il cambiamento anziché modificare l'obiettivo - e non usare mai lo stesso modello sia come generatore sia come giudice senza controlli incrociati, perché gonfia i punteggi.
- Fai: esegui il set di valutazione a ogni cambiamento di prompt/modello e blocca i merge sotto una soglia di faithfulness.
- Non fare: ritoccare le risposte di riferimento finché il modello attuale "passa".
Testa in produzione in sicurezza
Completa il testing pre-rilascio con una validazione controllata in produzione - canary release, feature flag, monitoraggio sintetico ed esperimenti A/B - per cogliere ciò che solo traffico e dati reali espongono. La produzione è l'ambiente definitivo, perciò validare lì con un raggio d'azione limitato e un rollback rapido colma il divario che gli ambienti di staging non possono.
- Fai: rilascia una nuova versione al 5% del traffico dietro un flag e osserva il tasso di errore e la latenza p99.
- Non fare: distribuire al 100% in un colpo solo senza canary, senza flag e senza un rollback provato.