Chiedi a un agente di coding di aggiungere i test a una funzione e in pochi secondi avrai una suite verde con copertura al 95%. Sembra perfetto. Poi qualcuno modifica la funzione, introduce un bug evidente, e i test continuano a passare. Il motivo è semplice: molti test generati verificano che il codice giri, non che faccia la cosa giusta. Chiamano la funzione, controllano che non lanci eccezioni, magari verificano il tipo del risultato. Non il suo valore.
Matt Pocock ha dedicato a questo problema un video del 14 ottobre, “Mutation testing is AWESOME”, proponendo una soluzione che esiste da decenni ma che con gli agenti torna di grande attualità: il mutation testing. L’idea è introdurre nel codice errori piccoli e controllati, i mutanti, e verificare se i test se ne accorgono. Se un mutante sopravvive, c’è un buco nei test.
In questa guida vediamo come funziona, quali strumenti usare per JavaScript, TypeScript, Python, Java e altri linguaggi, come integrarlo nel lavoro con gli agenti e come evitare che diventi lento e costoso.
Per un’azienda la domanda è semplice: come si fa a sapere che il software scritto con gli agenti, dal proprio team o da un fornitore, è stato verificato davvero? Una copertura dei test al 90% in un report non lo garantisce. Il mutation testing offre una misura più onesta, comprensibile anche a chi non legge il codice.
In breve
- Il mutation testing modifica il codice in modi piccoli (un
>che diventa>=, untrueche diventafalse) e riesegue i test. - Se i test falliscono, il mutante è ucciso; se passano, il mutante sopravvive e indica un test debole o mancante.
- Il mutation score misura l’efficacia dei test meglio della copertura, che dice solo quali righe sono state eseguite.
- Con gli agenti è doppiamente utile: misura la qualità dei test generati e dà all’agente un obiettivo concreto da migliorare.
- Strumenti principali: Stryker (JavaScript, TypeScript, C#, Scala), mutmut (Python), PIT (Java), cargo-mutants (Rust).
- Va usato in modo mirato: sui file modificati, in modalità incrementale, non su tutto il progetto a ogni commit.
- Per chi decide: il mutation score è un indicatore di qualità dei test più affidabile della copertura. Chiederlo per i moduli critici, come calcoli, permessi e fatturazione, è un modo semplice per valutare il lavoro di un fornitore.
Che cos’è il mutation testing
Il mutation testing è una tecnica che valuta la qualità dei test introducendo deliberatamente piccoli errori nel codice e controllando se i test li rilevano. Come spiega la documentazione di Stryker, ogni modifica produce un mutante; se almeno un test fallisce il mutante è ucciso, se tutti i test passano il mutante sopravvive. Più alta è la percentuale di mutanti uccisi, più efficaci sono i test.
Un esempio. Questa funzione calcola lo sconto per i clienti con un ordine sopra una soglia:
export function sconto(totale: number, clienteFedele: boolean): number {
if (totale >= 100 && clienteFedele) {
return totale * 0.1;
}
return 0;
}
Uno strumento di mutation testing genera varianti come queste:
totale >= 100diventatotale > 100&&diventa||totale * 0.1diventatotale / 0.1return 0diventa un blocco vuoto
Ora immagina che l’agente abbia scritto un solo test: “un cliente fedele con un ordine da 200 euro riceve uno sconto”. Il secondo mutante (||) sopravvive, perché con entrambe le condizioni vere il risultato non cambia. Il primo sopravvive anche lui, perché nessun test prova esattamente 100 euro. Due mutanti sopravvissuti, due casi limite non verificati. La copertura delle righe, nel frattempo, è al 100%.
Perché interessa a chi commissiona o gestisce software
I bug che costano sono nei calcoli
In un gestionale, in un e-commerce o in un portale clienti, gli errori più costosi raramente sono grafici. Sono nei calcoli e nelle regole: un listino applicato male, uno sconto cumulato per errore, un’aliquota IVA sbagliata, una provvigione calcolata sulla base sbagliata, un utente che vede dati di un altro cliente. Sono anche i punti in cui test superficiali passano senza accorgersi di nulla.
Un numero comprensibile a tutti
Il mutation score si spiega in una frase: “abbiamo introdotto di proposito cento piccoli errori nel modulo di fatturazione, e i test ne hanno intercettati ottantadue”. È un’informazione che un responsabile amministrativo o un direttore generale può valutare, a differenza di una percentuale di copertura delle righe.
Uno strumento per il collaudo e i contratti
Per i progetti affidati a un fornitore, si può concordare una soglia minima di mutation score sui moduli critici e chiedere il report alla consegna. Non serve imporla su tutto il progetto: basta sui moduli in cui un errore ha conseguenze economiche o legali.
Quali moduli proteggere per primi
- calcolo di prezzi, sconti, promozioni e spese di spedizione;
- fatturazione, IVA, note di credito;
- permessi e visibilità dei dati tra clienti o tra ruoli;
- integrazioni con gestionale, magazzino e contabilità;
- regole di business specifiche dell’azienda, come provvigioni o premi.
Il costo è limitato: tempo macchina per eseguire i test più volte, concentrato solo su questi moduli. Il beneficio è proporzionale al costo degli errori che si evitano.
Perché la copertura non basta, soprattutto con gli agenti
La copertura misura quali righe vengono eseguite durante i test. Non dice nulla su cosa i test verificano. Martin Fowler lo scriveva già anni fa nell’articolo Test Coverage: la copertura è utile per trovare parti di codice non testate, ma è un pessimo obiettivo numerico.
Con gli agenti il problema si amplifica per tre motivi.
- Gli agenti ottimizzano ciò che chiedi. Se chiedi “porta la copertura all’80%”, otterrai test che eseguono righe, non test che verificano comportamenti.
- I test nascono dal codice, non dai requisiti. Un agente che scrive i test dopo l’implementazione tende a verificare ciò che il codice fa, compresi i bug.
- Il volume rende impossibile rivedere tutto. Se l’agente scrive cento test in un pomeriggio, nessuno li legge uno per uno. Serve una misura automatica della loro qualità.
Il mutation testing risponde a tutti e tre i problemi: misura ciò che i test verificano davvero, non dipende da chi li ha scritti e si può automatizzare.
Gli esiti possibili di un mutante
Gli strumenti classificano ogni mutante in uno stato. Conoscerli aiuta a leggere i report.
| Stato | Significato | Cosa fare |
|---|---|---|
| Ucciso (killed) | Almeno un test è fallito | Niente: il test funziona |
| Sopravvissuto (survived) | Tutti i test sono passati | Aggiungere o rafforzare un test |
| Nessuna copertura (no coverage) | Nessun test esegue quel codice | Scrivere un test o valutare se il codice serve |
| Timeout | Il mutante ha causato un ciclo infinito | Di solito conta come ucciso |
| Errore di compilazione | Il mutante non compila | Ignorato nel punteggio |
| Equivalente | Il mutante si comporta come l’originale | Ignorarlo: nessun test può distinguerli |
I mutanti equivalenti meritano una nota. A volte una modifica non cambia il comportamento osservabile (per esempio in un ciclo dove < e != producono lo stesso risultato). Nessun test può ucciderli, e cercare di farlo è tempo perso. Per questo un mutation score del 100% raramente è un obiettivo sensato.
Gli strumenti per ogni linguaggio
| Linguaggio | Strumento | Note |
|---|---|---|
| JavaScript, TypeScript | StrykerJS | Supporta Jest, Vitest, Mocha, Karma; modalità incrementale |
| C# | Stryker.NET | Installabile come tool globale o di progetto |
| Scala | Stryker4s | Stessa famiglia, stessi report |
| Python | mutmut | Semplice da avviare, funziona con pytest |
| Java, JVM | PIT (pitest) | Lo standard de facto, integrazione Maven e Gradle |
| Rust | cargo-mutants | Si integra con cargo, adatto alla CI |
Per un team che lavora su web app con TypeScript, StrykerJS è la scelta naturale. Matt Pocock lo usa nel video Mutation testing is AWESOME proprio per mostrare come rafforzare la suite di test scritta da un agente.
Configurare StrykerJS in un progetto TypeScript
L’installazione più rapida usa l’inizializzatore, che fa alcune domande e crea il file di configurazione:
npm init stryker@latest
# oppure, con pnpm
pnpm create stryker@latest
Una configurazione essenziale per un progetto con Vitest:
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "vitest",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.d.ts"],
"reporters": ["html", "clear-text", "progress"],
"thresholds": { "high": 80, "low": 60, "break": 50 },
"incremental": true
}
Tre impostazioni contano più delle altre.
- mutate: limita i file da mutare. Escludi test, tipi, file generati, configurazioni. Mutare ciò che non contiene logica spreca tempo.
- thresholds:
highelowcolorano il report,breakfa fallire il comando se il punteggio scende sotto la soglia. Utile in CI. La pagina sulla configurazione descrive tutte le opzioni. - incremental: salva i risultati e, alla corsa successiva, rianalizza solo i mutanti toccati dalle modifiche. È la differenza tra minuti e ore su un progetto grande. I dettagli sono nella documentazione della modalità incrementale.
Il report HTML mostra il codice con i mutanti sopravvissuti evidenziati riga per riga: è il formato più comodo per una persona e, come vedremo, anche per un agente.
Come usarlo con un agente di coding
Il mutation testing non serve solo a misurare. Con un agente diventa un ciclo di miglioramento: l’agente scrive i test, lo strumento trova i mutanti sopravvissuti, l’agente scrive test per ucciderli. È un esempio perfetto di sensore nell’harness di un agente, come abbiamo descritto nella guida all’harness engineering: un controllo deterministico che restituisce un segnale preciso su cui l’agente può lavorare.
Il ciclo in pratica
- L’agente implementa la funzionalità e scrive i test.
- Si esegue Stryker solo sui file modificati.
- L’elenco dei mutanti sopravvissuti (file, riga, mutazione) viene passato all’agente.
- Per ogni mutante, l’agente decide: scrivere un test che lo uccide, oppure segnalarlo come equivalente con una motivazione.
- Si riesegue Stryker. Si ripete finché il punteggio supera la soglia concordata.
Istruzione per l'agente (esempio)
Esegui `pnpm stryker run --mutate "src/ordini/sconto.ts"`.
Per ogni mutante sopravvissuto nel report:
- se rappresenta un comportamento che conta, scrivi un test che fallisce
con il mutante e passa con il codice originale;
- se è equivalente, non scrivere test: elencalo con una riga di spiegazione.
Non modificare il codice di produzione per far morire i mutanti.
Fermati quando il mutation score del file è almeno 80%.
L’ultima regola è importante. Un agente sotto pressione per far salire un numero può trovare scorciatoie, come semplificare il codice per eliminare rami difficili da testare. Il codice di produzione deve restare invariato in questa fase.
Una skill dedicata
Se il ciclo diventa abituale, conviene trasformarlo in una skill: comando da eseguire, formato del report, regole su mutanti equivalenti e soglia. Ne abbiamo parlato nella guida alle agent skills; è un caso tipico di procedura a bassa libertà che beneficia di istruzioni precise.
Il mutation testing nelle grandi aziende
Il mutation testing ha avuto a lungo fama di tecnica accademica, troppo lenta per i progetti reali. Due casi mostrano che non è più così.
Google lo integra nella revisione del codice. Il paper State of Mutation Testing at Google descrive un sistema usato da circa 6.000 ingegneri, che lavora solo sulle modifiche in revisione e scarta le righe considerate poco interessanti (log, configurazioni, codice banale) per mostrare pochi mutanti significativi. Il principio è fondamentale: non bombardare gli sviluppatori di risultati, ma mostrare quelli che contano.
Meta ha fatto un passo ulteriore con ACH, presentato nel post LLM-powered bug catchers e nel paper Mutation-Guided LLM-based Test Generation at Meta. Un ingegnere descrive in linguaggio naturale il tipo di bug che lo preoccupa (per esempio un problema di privacy), un LLM genera mutanti realistici di quel tipo e poi i test che li uccidono. È lo stesso ciclo che abbiamo descritto sopra, con l’aggiunta di mutanti mirati invece che generici.
Leggere un report: cosa guardare per primo
Il primo report di mutation testing su un progetto reale può scoraggiare: centinaia di mutanti sopravvissuti, punteggi bassi, colori rossi ovunque. Non serve affrontare tutto. Ecco un ordine di lettura che funziona.
- Parti dai file più importanti, non da quelli con il punteggio più basso. Un mutante sopravvissuto nel calcolo dell’IVA conta più di dieci in una funzione di formattazione.
- Guarda prima i mutanti “nessuna copertura”. Indicano codice che nessun test esegue: a volte è codice morto da eliminare, a volte un ramo critico dimenticato.
- Poi i mutanti sui confini:
>che diventa>=,<che diventa<=. Sono quasi sempre casi limite reali non verificati, come la soglia esatta di 100 euro dell’esempio. - Poi le condizioni logiche:
&&che diventa||, una condizione negata, un ramo rimosso. Indicano combinazioni di input mai provate. - Infine i valori restituiti: un risultato sostituito con zero, una stringa vuota, un array vuoto. Se sopravvivono, i test non controllano il valore del risultato.
Per ogni mutante sopravvissuto la domanda giusta non è “come lo uccido?” ma “quale comportamento non sto verificando?”. La risposta spesso porta a un test che ha senso di per sé, indipendentemente dal punteggio.
Mutanti mirati: la lezione di Meta applicata ai tuoi progetti
Gli strumenti classici generano mutanti con regole fisse: cambiano operatori, valori, condizioni. Funzionano bene, ma non sanno nulla del tuo dominio. L’approccio di Meta suggerisce un’alternativa che si può replicare in piccolo con qualsiasi agente di coding: chiedere all’agente di generare bug realistici per il tuo dominio.
Per un gestionale di ordini, per esempio, i bug che preoccupano davvero sono specifici: un’aliquota IVA applicata due volte, un ordine annullato che finisce comunque in fattura, un utente di un’azienda che vede gli ordini di un’altra, un arrotondamento fatto sulla riga invece che sul totale. Nessun mutatore generico li produce.
Istruzione per l'agente (esempio)
Leggi src/fatturazione/. Proponi 10 bug realistici che uno sviluppatore
potrebbe introdurre in questo modulo (errori di arrotondamento, aliquote,
stati dell'ordine, permessi tra aziende diverse).
Per ciascuno: applica il bug in un ramo temporaneo, esegui i test,
annota se almeno un test fallisce. Poi ripristina il codice.
Alla fine elenca i bug che i test NON rilevano e proponi un test per ognuno.
È un esercizio che richiede mezz’ora e spesso rivela lacune che nessuna metrica avrebbe mostrato. Combina bene con il mutation testing classico: lo strumento trova i buchi meccanici, l’agente quelli di dominio.
Errori comuni
Mutare tutto il progetto a ogni commit
Il mutation testing riesegue i test per ogni mutante: su un progetto grande può richiedere ore. Va limitato ai file modificati, con la modalità incrementale.
Inseguire il 100%
I mutanti equivalenti rendono il punteggio perfetto irraggiungibile o costoso. Una soglia tra il 60% e l’80% sulle parti importanti è un obiettivo realistico.
Mutare codice senza logica
Configurazioni, costanti, file generati, componenti puramente visivi: producono mutanti inutili e rumore nei report.
Lasciare che l’agente “aggiusti” il codice
Se l’obiettivo è il punteggio, l’agente può modificare il codice di produzione per eliminare i rami scomodi. Le istruzioni devono vietarlo esplicitamente.
Test troppo legati all’implementazione
Per uccidere un mutante si può scrivere un test che verifica dettagli interni invece del comportamento. Funziona per il punteggio, ma rende fragile la suite. I test devono restare sul comportamento osservabile.
Suite di test lente
Se un’esecuzione dei test richiede due minuti, mille mutanti richiedono ore. Prima di introdurre il mutation testing, rendi veloci i test unitari.
Portarlo in produzione nel team
- Dove applicarlo: logica di dominio, calcoli, regole di business, permessi, validazioni. Non interfacce, configurazioni o codice di integrazione puro.
- Quando eseguirlo: in locale o dall’agente sui file modificati; in CI sulle pull request, in modalità incrementale; su tutto il progetto una volta a settimana in un job notturno.
- Soglie: parti con una soglia di blocco bassa (per esempio 50%) solo sui moduli critici e alzala gradualmente.
- Report visibili: pubblica il report HTML come artefatto della CI, così chi rivede la pull request vede i mutanti sopravvissuti.
- Costo: il tempo macchina conta, soprattutto in CI. Misura la durata e ottimizza prima di estendere.
I test del tuo progetto passano sempre ma i bug arrivano lo stesso in produzione? Nei progetti di sviluppo web app e di software gestionale impostiamo strategie di test misurabili, anche quando gran parte del codice è scritta con gli agenti.
Da dove partire questa settimana
- Scegli un modulo con logica importante, per esempio calcoli di prezzi o permessi.
- Installa lo strumento per il tuo linguaggio ed eseguilo solo su quel modulo.
- Guarda il report: quanti mutanti sopravvivono? Quali comportamenti non sono verificati?
- Chiedi all’agente di uccidere i mutanti sopravvissuti senza toccare il codice di produzione.
- Aggiungi l’esecuzione incrementale alla CI per quel modulo, con una soglia di blocco prudente.
Checklist per introdurre il mutation testing
- La suite di test unitari è veloce (pochi secondi per modulo).
- Il mutation testing è limitato ai file con logica di dominio.
- Test, tipi, configurazioni e file generati sono esclusi.
- La modalità incrementale è attiva.
- Le soglie sono realistiche e partono dai moduli critici.
- Il report è visibile nella pull request.
- L’agente ha istruzioni per uccidere i mutanti senza modificare il codice di produzione.
- I mutanti equivalenti vengono segnalati, non inseguiti.
- I nuovi test verificano comportamenti, non dettagli interni.
- Un’esecuzione completa periodica controlla l’andamento nel tempo.
Domande frequenti
Che differenza c’è tra mutation testing e copertura del codice?
La copertura misura quali righe vengono eseguite dai test. Il mutation testing misura se i test si accorgono quando il codice cambia comportamento. Una funzione può avere copertura del 100% e test che non verificano nulla di utile: il mutation testing lo rivela.
Il mutation testing è troppo lento per i progetti reali?
Su tutto il progetto e a ogni commit sì. Usato sui soli file modificati, con la modalità incrementale e su test unitari veloci, richiede pochi minuti. È lo stesso approccio che Google usa nella revisione del codice.
Qual è un buon mutation score?
Dipende dal codice. Per la logica di dominio importante, tra il 60% e l’80% è un obiettivo realistico. Il 100% è raramente sensato, perché alcuni mutanti sono equivalenti all’originale e nessun test può distinguerli.
Posso far scrivere all’agente i test per uccidere i mutanti?
Sì, ed è uno degli usi migliori. Passa all’agente l’elenco dei mutanti sopravvissuti e chiedigli di scrivere test che falliscono con il mutante e passano con il codice originale, vietando modifiche al codice di produzione.
Quale strumento usare per TypeScript?
StrykerJS è il più diffuso: supporta i principali test runner come Jest e Vitest, ha una modalità incrementale e produce report HTML chiari. Per Python c’è mutmut, per Java PIT, per Rust cargo-mutants.
Il mutation testing sostituisce la revisione dei test?
No, la rende più efficiente. Indica dove i test sono deboli, ma non dice se i test verificano i requisiti giusti. Una revisione umana dei casi principali resta utile, soprattutto per la logica di business.
Il mutation testing ha senso anche per progetti piccoli?
Sì, se il progetto contiene logica che conta: calcoli di prezzi, regole di business, permessi. Su un progetto piccolo l’esecuzione è rapida e il beneficio immediato. Ha poco senso invece su siti con pochissima logica, dove i test end-to-end delle pagine principali sono più utili.
Come valuto la qualità dei test se non sono un tecnico?
Chiedi il mutation score dei moduli critici e un elenco dei principali comportamenti non verificati. Un fornitore che lavora bene può spiegarti in poche frasi quali errori i test intercettano e quali no. Diffida dei report che mostrano solo la copertura delle righe.
Fonti e approfondimenti
- Documentazione Stryker Mutator
- Stryker: elenco dei mutatori supportati
- Google Research: State of Mutation Testing at Google
- Meta: Mutation-Guided LLM-based Test Generation (arXiv)
- PIT: mutation testing per Java
- Guida pratica a Stryker e soglie in CI
Vuoi test che trovano i bug, non solo test che passano? Raccontaci il tuo progetto e impostiamo insieme una strategia di qualità misurabile.