AI-Generated Test Cases: Using LLM as an Automatic Test Writer
Writing tests is boring, time consuming and procrastinating. How many times have you seen a PR without a single new test why did I "add it to the backlog"? LLMs are changing this scenario: GitHub Copilot, Claude and GPT-4 can generate test cases of surprising quality starting from source code, user stories or API specifications — reducing writing time tests by 40-60% and lowering the psychological barrier that leads to skipping them.
But it's not enough to ask AI to "write the tests" and blindly use them. This guide shows a structured workflow to generate high-quality tests, validate them and integrate them into the CI pipeline.
What You Will Learn
- Prompting strategies to generate effective test cases
- GitHub Copilot workflow for unit testing from source code
- Test generation from user stories and BDD specifications
- How to validate the quality of tests generated by AI
- Integration with mutation testing to verify the real value
- Limits and risks: when AI tests are not enough
The Problem with Tests Written by AI Without Verification
Before we get into the workflow, a critical warning: LLMs generate tests that they pass
easily but not necessarily test the right behavior. The most common pattern is the
"phantom test" — a test that checks that result !== null instead of checking
the real value.
// ESEMPIO DI TEST AI GENERATO SCARSO (da evitare)
// Copilot ha generato questo — sembra un test, ma non verifica nulla di utile
import { calculateDiscount } from './pricing';
describe('calculateDiscount', () => {
it('should return a value', () => {
const result = calculateDiscount(100, 'PROMO10');
expect(result).toBeDefined(); // INUTILE: non verifica il valore corretto
expect(result).not.toBeNull(); // INUTILE: solo dice "non e null"
expect(typeof result).toBe('number'); // DEBOLE: non verifica che sia 90
});
});
// ESEMPIO DI TEST AI GENERATO BUONO (con prompt corretto)
describe('calculateDiscount', () => {
it('applica sconto PROMO10 del 10% sul prezzo base', () => {
expect(calculateDiscount(100, 'PROMO10')).toBe(90);
});
it('restituisce il prezzo originale per codice non valido', () => {
expect(calculateDiscount(100, 'INVALID')).toBe(100);
});
it('applica sconto massimo del 50% anche per codici piu generosi', () => {
expect(calculateDiscount(100, 'SUPER80')).toBe(50); // cap al 50%
});
it('gestisce prezzi a zero senza errori', () => {
expect(calculateDiscount(0, 'PROMO10')).toBe(0);
});
it('lancia errore per prezzi negativi', () => {
expect(() => calculateDiscount(-10, 'PROMO10')).toThrow(RangeError);
});
});
Effective Prompting for GitHub Copilot
The quality of the generated tests directly depends on the quality of the prompt. Here are the strategies more effective:
// STRATEGIA 1: Commento contestuale nel file di test
// Il commento guida Copilot nel generare test completi
// src/utils/string-validator.ts
export function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function validatePassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) errors.push('Minimo 8 caratteri');
if (!/[A-Z]/.test(password)) errors.push('Almeno una maiuscola');
if (!/[0-9]/.test(password)) errors.push('Almeno un numero');
if (!/[!@#$%]/.test(password)) errors.push('Almeno un carattere speciale');
return { valid: errors.length === 0, errors };
}
// src/utils/string-validator.test.ts
// Test per validateEmail e validatePassword.
// Coprire: email valide, email non valide (mancano @, dominio, TLD),
// password valida, password troppo corta, password senza maiuscola,
// senza numero, senza speciale, stringa vuota, valori limite.
// Usare describe nested per raggruppare casi validi e invalidi.
// I test devono essere deterministici, no mocks necessari.
import { validateEmail, validatePassword } from './string-validator';
describe('validateEmail', () => {
describe('email valide', () => {
test.each([
['user@example.com'],
['user.name+tag@company.co.uk'],
['123@numbers.org'],
])('%s deve essere valida', (email) => {
expect(validateEmail(email)).toBe(true);
});
});
describe('email non valide', () => {
test.each([
['not-an-email'],
['missing@'],
['@nodomain.com'],
['spaces in@email.com'],
[''],
])('%s deve essere non valida', (email) => {
expect(validateEmail(email)).toBe(false);
});
});
});
describe('validatePassword', () => {
it('password valida supera tutti i controlli', () => {
const result = validatePassword('SecureP@ss1');
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('password troppo corta produce errore specifico', () => {
const result = validatePassword('Ab1@');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Minimo 8 caratteri');
});
});
Generation from User Stories with Claude
Claude is particularly effective at transforming user stories into structured test cases, including edge cases that developers tend to forget.
"""
Prompt per Claude: Generazione test da User Story
"""
user_story = """
User Story: Come utente registrato, voglio aggiungere prodotti al carrello
in modo da poter procedere all'acquisto.
Criteri di accettazione:
- L'utente puo aggiungere un prodotto disponibile al carrello
- Il contatore nel header si aggiorna immediatamente
- Non si possono aggiungere prodotti esauriti
- La quantita massima per prodotto e 10
- Un prodotto gia nel carrello aumenta la quantita (non crea duplicato)
- I prodotti nel carrello persistono dopo refresh della pagina
"""
prompt = f"""Sei un QA engineer esperto in BDD e test automation.
Data la seguente user story e criteri di accettazione, genera:
1. Casi di test Playwright in TypeScript per i test E2E
2. Unit test per la funzione addToCart(productId, quantity) in Vitest
3. Test cases in formato Gherkin (BDD) per la documentazione
Per ogni test specifica:
- Scenario (nome descrittivo)
- Precondizioni
- Azioni
- Risultato atteso
Includi casi happy path, edge cases e casi di errore.
User Story:
{user_story}
Genera i test nel formato richiesto, con commenti che spiegano il
rationale di ogni caso di test."""
# Risposta Claude genera 15-20 test cases strutturati con Playwright + Vitest + Gherkin
# Esempio output Claude in Gherkin
Feature: Aggiunta prodotti al carrello
Background:
Given l'utente e autenticato
And il catalogo prodotti e disponibile
Scenario: Aggiunta prodotto disponibile
When l'utente clicca "Aggiungi al carrello" sul prodotto "Laptop Pro X"
Then il carrello contiene 1 unita di "Laptop Pro X"
And il contatore nell'header mostra "1"
Scenario: Prodotto gia nel carrello
Given il carrello contiene 1 unita di "Laptop Pro X"
When l'utente clicca "Aggiungi al carrello" sullo stesso prodotto
Then il carrello contiene 2 unita di "Laptop Pro X"
And non viene creato un record duplicato
Scenario: Tentativo aggiunta prodotto esaurito
Given il prodotto "Tablet Z" ha disponibilita 0
Then il bottone "Aggiungi al carrello" per "Tablet Z" e disabilitato
Scenario: Limite quantita massima
Given il carrello contiene 10 unita di "Mouse Wireless"
When l'utente tenta di aggiungere un'altra unita
Then viene mostrato il messaggio "Quantita massima raggiunta (10)"
And la quantita rimane 10
Complete Workflow: From Code to Validated Tests
// Workflow in 5 step per test AI-generated di qualita
// STEP 1: Genera i test
// Prompt: "Genera test completi per questo servizio con casi happy path,
// edge cases, casi di errore. Usa Vitest e TypeScript."
// [AI genera tests/payment.service.test.ts]
// STEP 2: Esegui i test generati
// npm run test -- tests/payment.service.test.ts
// ATTESO: tutti passano (se l'implementazione e corretta)
// STEP 3: Verifica mutation score
// npx stryker run -- solo sui file modificati
// TARGET: mutation score > 60% sui test AI-generated
// STEP 4: Review manuale
const REVIEW_CHECKLIST = `
[ ] I test coprono i criteri di accettazione?
[ ] I test verificano il comportamento (non l'implementazione)?
[ ] Ci sono edge cases mancanti? (null, undefined, stringhe vuote, valori limite)
[ ] I test sono deterministici? (no Date.now(), no Math.random() senza mock)
[ ] I test sono isolati? (no dipendenze tra test)
[ ] I nomi dei test descrivono il comportamento atteso?
[ ] I test falliscono correttamente se implementazione e errata?
`;
// STEP 5: Aggiungi test mancanti
// Usa mutation report per identificare mutanti non uccisi
// Aggiungi test mirati per i mutanti sopravvissuti
Validate AI Tests with Mutation Testing
The most reliable way to verify that AI-generated tests have real value is perform mutation testing on them:
// stryker.config.js — configurazione per analizzare test AI-generated
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
module.exports = {
testRunner: 'vitest',
coverageAnalysis: 'perTest',
mutate: [
// Muta solo i file per cui stiamo analizzando i test
'src/services/payment.service.ts',
'src/utils/pricing.ts'
],
vitest: {
configFile: 'vitest.config.ts'
},
thresholds: {
high: 80,
low: 60,
break: 50 // Fallisce la build se mutation score scende sotto 50%
},
reporters: ['html', 'json', 'clear-text'],
htmlReporter: {
fileName: 'reports/mutation-report.html'
}
};
Risks and Limits of AI Test Generation
- Tests that test the implementation: AI tends to replicate logic of code in tests instead of verifying behavior. Review the tests they look like "mirror" of the source code
- Lack of domain knowledge: AI doesn't know business rules of your specific domain. Critical business logic tests must be written or reviewed from a developer with context
- False assurance: Generated tests that pass do not mean useful tests. Always perform mutation testing before considering full coverage
- Hallucination in fixtures: AI can generate test data that looks like valid but they are not (impossible dates, numbers outside the range, emails formally valid but semantically incorrect)
Conclusions
AI test generation is a powerful tool when used correctly: it speeds up writing of boilerplate, generates edge cases that are easily forgotten and lowers the barrier psychology to testing. But it requires a validation workflow — mutation testing and review manual — to ensure that the generated tests really have value.
The rule of thumb: uses AI to generate 80% of boilerplate and obvious cases; You write the 20% that require domain knowledge.







