Test assistiti dall'IA: usare i gate dei test unitari per coprire il codice generato dall'IA
Categories:
I test assistiti dall’IA non risolvono il problema della copertura dei test, ma il mio problema di fiducia nel codice generato dall’IA. L’IA in tre minuti butta fuori un grande blocco di codice, mentre una persona ci mette due ore per revisionarlo tutto; tre minuti per generare, due ore per revisionare non è un’esagerazione. La mia soluzione è cambiare l’oggetto della revisione: non leggere riga per riga il codice, ma leggere i documenti dei casi di test, e poi usare i test unitari nell’ambiente di sviluppo locale per creare un gate, lasciando che i casi di test falliti monitorino al posto mio la pigrizia e gli errori dell’IA.
I piccoli bug sono il vero campo di battaglia dei test assistiti dall’IA
I bug si dividono in due categorie: bug facili da risolvere e bug difficili da risolvere.
I bug difficili da risolvere derivano solitamente da difetti di architettura o progettazione della soluzione, che si evolvono gradualmente in debiti tecnici, dove tirare un filo muove tutto il corpo; oppure possono essere causati da terze parti, come librerie di terze parti, sistemi operativi, compilatori, e richiedono conoscenze più specialistiche e un’indagine più approfondita per essere risolti. Questo tipo di problemi complessi non rientra nell’ambito di questo articolo.
Questo articolo si concentra sui piccoli bug nel ciclo di vita del prodotto, sfruttando l’IA per assistere la creazione dei casi di test, per risolvere i seguenti problemi:
- Bug che possono essere scoperti rapidamente
- Bug banali che richiedono solo minimi cambiamenti per essere corretti
- Nuovi bug introdotti dalle modifiche
- Errori commessi nella fretta durante le correzioni urgenti
- Il compromesso tra costo ed efficienza dei test mirati
La maggior parte dei bug sono piccoli bug, ma i piccoli bug possono anche causare problemi seri. Risolvere i piccoli bug in locale può risparmiare enormi quantità di sforzo successivo.
Spostare l’oggetto della revisione dal codice ai documenti dei casi di test
Ho provato un’idea molto audace: lasciare che l’IA completasse un requisito interamente da sola, senza revisionare affatto il codice generato, in modo simile alla modalità Copilot Agent che lascia completamente le redini. Poi ho rinunciato. La ragione fondamentale è che non avevo fiducia. Per un programma binario rigorosamente logico, fatto di 0 e 1, non avere fiducia significa perdere la fiducia di monte e di valle, perdere la fiducia dei clienti. Per questo sto imparando a pensare al vibe coding: per il codice generato dall’IA, bisogna assolutamente avere fiducia.
Il problema sta nel costo della revisione. L’IA a volte genera o modifica una quantità di codice troppo grande: tre minuti per generare, due ore per revisionare non è un’esagerazione. Il lavoro per il codice di test è solitamente ancora maggiore di quello per il codice di business, e spesso non viene conteggiato nella valutazione della consegna degli sviluppatori, ed è uno dei motivi per cui molti non vogliono scrivere test proattivamente. Ma il fatto che l’assistente IA sia molto più intelligente di me è ormai assodato; invece di resistere, meglio dargli un gate.
Spostare l’oggetto della revisione dal codice ai documenti dei casi di test è la chiave per sciogliere questo nodo. Diversamente dal codice di business, i casi di test generati dall’IA descrivono solitamente uno scenario molto specifico, facile da revisionare e verificare. Molti casi non vanno nemmeno letti riga per riga, basta guardarli quando falliscono. Dire di non revisionare affatto il codice IA è un’esagerazione, ma grazie a documenti dei casi di test ben fatti, si può dimostrare che non ogni riga di codice richiede una revisione manuale.
---
title: Dalla revisione del codice alla revisione dei documenti
---
flowchart TB
A[IA genera molto codice] e1@--> B[Costo alto revisione riga per riga]
B e2@--> C{Cosa revisionare}
C e3@-->|Codice| D[Due ore revisione, nessuna fiducia]
C e4@-->|Documenti casi di test| E[Facile da revisionare, verifica rapida]
E e5@--> F[Gate test unitari]
F e6@--> G[Casi falliti espongono problemi]
classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class D risk;
class E,F,G ok;
class e1,e2,e3,e4,e5,e6 animate;Base metodologica: Six Sigma e DMAIC
Il Six Sigma è una metodologia di miglioramento della qualità basata sui dati, il cui obiettivo principale è controllare il tasso di difetti a 3,4 parti per milione, ovvero al livello 6σ, riducendo la variabilità del processo1.
Fornisce un processo eseguibile, il DMAIC:
- Define (Definire): analisi dei requisiti, conversione in modelli di dati, progettazione di strutture dati e interfacce
- Measure (Misurare): monitoraggio dei casi di test, copertura, prestazioni, sicurezza
- Analyze (Analizzare): analisi dei casi di test e del codice di business
- Improve (Migliorare): miglioramento dei casi di test e del codice di business
- Control (Controllare): gate di test automatizzati
Gli strumenti tecnici di supporto includono linter, analisi statica, analisi dinamica e framework di test. Sono molti gli indicatori di qualità del codice su cui ci si può concentrare, ad esempio indicatori di complessità (complessità ciclica, complessità cognitiva, profondità di annidamento, accoppiamento tra classi), indicatori di qualità (tasso di duplicazione, lunghezza delle funzioni, numeri magici), e indicatori di test (copertura test unitari, tasso di superamento test di integrazione, efficacia dei casi di test, copertura gestione eccezioni).
Metodologia e strumenti aiutano a scoprire e risolvere i problemi, al prezzo di un possibile aumento dei costi di consegna e una riduzione dell’efficienza. Con risorse limitate, bisogna scegliere un metodo che porti benefici sostenibili nel breve, medio e lungo termine. La mia scelta è usare i test unitari come strumento ausiliario nel processo di sviluppo.
Sfruttando la capacità dell’IA di convertire tra linguaggi di ruoli diversi, lo sviluppo può fondere i vantaggi di TDD, BDD, ATDD, generando rapidamente tabelle dei casi di test e codice dei casi di test, migliorando la qualità della consegna.
Sviluppo guidato dai test: TDD/BDD/ATDD e codice testabile
Le differenze tra i tre tipi di sviluppo guidato dai test sono le seguenti:
| Dimensione | TDD | BDD | ATDD |
|---|---|---|---|
| Origine guida | Test unitari | Scenari di comportamento utente | Condizioni di accettazione |
| Partecipanti | Sviluppatori | Business + Sviluppo | Business + Test + Sviluppo |
| Livello test | Test unitari | Test di integrazione / end-to-end | Test di accettazione |
| Esempi strumenti | JUnit, pytest | Cucumber, SpecFlow | FitNesse, Robot Framework |
| Output tipico | Report copertura codice | Documenti scenari utente eseguibili | Tasso superamento test accettazione |
| Linguaggio | assert add(2,3) == 5 | Linguaggio naturale scenari (Given-When-Then) | Linguaggio business (tabelle condizioni accettazione) |
Il codice testabile ha quattro principi fondamentali: responsabilità singola (ogni funzione fa una sola cosa), iniezione delle dipendenze (passare le dipendenze tramite parametri), isolamento delle interfacce (tipi di input/output chiari), osservabilità dello stato (fornire interfacce di query per verificare lo stato interno).
Nei prompt per l’IA, suggerisco di scrivere direttamente estendibile, responsabilità singola, iniezione dipendenze. Ho provato a far controllare all’IA la lunghezza delle funzioni e dei file generate, ma la sua aderenza era scarsa; passando a responsabilità singola e iniezione dipendenze, il codice generato è diventato notevolmente più allineato alle aspettative. L’ottimizzazione prematura è la radice di tutti i mali; quando si generano strutture dati e interfacce di base, basta chiedere all’IA di dare priorità all’estendibilità.
Esempio di codice facile da testare:
// user.h
typedef struct {
int id;
char name[50];
int age;
} User;
// Interfaccia astratta storage in memoria
typedef struct {
User* (*get_user)(int id);
int (*save_user)(const User* user);
} UserStorage;
// Crea utente (iniezione dipendenza interfaccia storage)
User* create_user(const char* name, int age, UserStorage* storage) {
if(strlen(name) >= 50 || age <= 0) return NULL;
User* new_user = malloc(sizeof(User));
new_user->id = generate_user_id();
strncpy(new_user->name, name, 49);
new_user->name[49] = '\0';
new_user->age = age;
if(storage->save_user(new_user) != 0) {
free(new_user);
return NULL;
}
return new_user;
}
Esempio di codice difficile da testare:
// Operazioni database strettamente accoppiate
void save_user_to_db(User* user) {
MYSQL conn;
mysql_init(&conn);
if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
log_error("DB connection failed"); // Dipendenza log difficile da testare
exit(1); // Uscita diretta che influisce sull'esecuzione dei test
}
// Costruzione SQL e scrittura su database
}
Il buon codice agisce in silenzio, il cattivo codice ha una forte presenza. Le funzioni facili da testare hanno valori di ritorno chiari, gestione errori standardizzata, parametri di input espliciti, effetti collaterali isolati, supportano l’iniezione delle dipendenze.
Il codice fornito dall’IA a volte non segue le best practice e ha problemi di stile. Anche se spesso risolve il problema, i progetti reali non sono affari una tantum, bisogna considerare la manutenzione successiva. Quando si chiede all’IA di generare codice, bisogna insistere su leggibilità prioritaria, estendibile, responsabilità singola, iniezione dipendenze. I problemi di stile del codice non vengono discussi oltre, si può fare riferimento alle linee guida di stile di Google2.
Trappole e compromessi nello sviluppo reale
Quando i requisiti passano tra ruoli diversi, subiscono molteplici conversioni linguistiche, e ogni passaggio può introdurre imprecisioni. Ad esempio, il “visualizzare in modo centralizzato” detto dal cliente viene interpretato dal backend come “restituzione tramite singola interfaccia” senza paginazione. Sfruttando l’IA per generare modelli linguistici per i vari ruoli, si può ridurre la perdita di informazioni dovuta alla conversione manuale.
La mancanza di riflessione è la radice di molti bug. Sfruttando l’IA per progettare gli scenari dei casi di test, questa non solo completa i compiti previsti, ma spesso supera le aspettative dando spunti e colmando dettagli che chi ha dato l’incarico non aveva considerato. In ogni conversione linguistica tra ruoli, si può dialogare con l’IA per colmare le falle. Il Markdown è il linguaggio che l’IA capisce meglio, input e output dovrebbero adottare quanto possibile il formato Markdown.
Ci sono diversi punti critici per il codice dei casi di test: isolamento dei casi, accuratezza delle dipendenze simulate, validità delle asserzioni, valori limite, gestione eccezioni, prestazioni, concorrenza. L’IA è pigra e non genera in una volta casi che coprano tutti gli scenari. Generare tutto il codice di test in una volta richiede molto al modello; si può generare in più volte.
Prendendo come esempio il modello Claude in Cursor, gestisce bene alcune accortezze sul codice dei casi di test, ma dimentica spesso l’isolamento dei casi e gli scenari eccezionali. Aggiungere una user rule può ridurre leggermente la pigrizia: quando generi casi di test, fai attenzione al documento dei casi, isolamento dei casi. Tuttavia, non genererà comunque tutti gli scenari in una volta; percorsi normali, percorsi eccezionali, valori limite, prestazioni, concorrenza e altri scenari richiedono più round di dialogo per essere integrati.
Un flusso di lavoro fattibile per i test assistiti dall’IA
Combinando i documenti di progettazione, si può creare una pipeline dallo scenario al caso di test:
---
title: Flusso di lavoro test assistiti dall'IA
---
flowchart TB
A[Documento progettazione] e1@--> B[Genera scenari test accettazione]
B e2@--> C[Genera strutture dati e interfacce]
C e3@--> D[Genera tabella casi lato client]
D e4@--> E[Genera codice casi di test]
D e5@--> F[Genera codice business]
E e6@--> G[Gate locale: casi superati]
F e7@--> G
G e8@--> H{Esistono casi falliti}
H e9@-->|Sì| E
H e10@-->|No| I[Invio per test]
classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A input;
class B,C,D,E,F gen;
class G,H,I gate;
class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;I prompt per i vari passaggi possono essere organizzati così: generare scenari di test di accettazione basandosi sul documento di progettazione del prodotto; generare strutture dati e interfacce basandosi sugli scenari di test; generare tabella casi lato client basandosi su scenari di test e interfacce; generare codice casi di test basandosi su interfacce e tabella casi; generare codice business basandosi sugli scenari di test assicurando che i casi passino.
Qui c’è un rischio: l’IA potrebbe modificare il codice business basandosi sul codice di test, oppure viceversa modificare il codice di test basandosi sul codice business. Se entrambi hanno errori, l’IA si confonde, e con l’aumentare dei round di dialogo, il codice di test e il codice business diventano sempre più caotici; a quel punto è necessario l’intervento manuale.
Un prompt troppo ambizioso è un controesempio: chiedere all’IA di coprire in una volta normale, eccezioni, valori limite, concorrenza e generare codice. Questo tipo di richiesta si interrompe facilmente. Il modo corretto è in Cursor separare gli scenari, usando più richieste per generare a batch. I casi di test di prestazioni richiedono grandi quantità di dati, vanno lasciati alla fase di test di sistema: prima far scrivere all’IA un generatore di dati casuali, poi usare i dati generati per costruire i casi.
I test di integrazione richiedono la costruzione dell’ambiente di test. Alcuni casi dipendono da strumenti esterni, ad esempio la micro-segmentazione dipende da iptables e ipset; si possono usare container Docker per costruire l’ambiente di test. Il principio è: come testa una persona, così si comanda all’IA di implementare gli stessi passaggi automatizzati. Per i test di sicurezza, al momento ci si basa principalmente su esperienza e analisi, non ci sono ancora esperienze mature di assistenza IA.
Conclusione
Questo flusso di lavoro, rispetto al non usare l’IA, produce più documenti e codice di test, ed è difficile revisionare ogni singola parola. Ma i documenti sono più facili da revisionare del codice, soprattutto le tabelle dei casi, che hanno un ruolo di guida preciso per la generazione dei casi. Nello sviluppo reale, a volte revisiono solo i casi falliti, confermando i problemi attraverso il debug dei casi falliti.
Le direzioni di miglioramento sono tre: il sotto-flusso della fase di progettazione ha sovrapposizioni, gli scenari di test di accettazione potrebbero dipendere da strutture dati e interfacce; la tabella casi lato client è curata direttamente dagli sviluppatori; prima dell’invio per test serve rigenerare scenari di test dettagliati che includano i dettagli del codice business.
Il guadagno finale non sta nei numeri della copertura dei test, ma nella fiducia: grazie a questo gate locale dei test unitari, finalmente ho fiducia nel codice generato dall’IA, invece di mandare in produzione il codice uscito dal vibing senza pensarci su.
La definizione di Six Sigma e il metodo DMAIC fanno riferimento alla voce Wikipedia. ↩︎
Google Style Guide è la guida di stile del codice pubblicata da Google. ↩︎