05 - Testare il Codice Generato da AI: Strategie e Framework per la qualità
Il 2025 ha sancito una verita scomoda: il codice generato da AI e funzionale, rapido da produrre, spesso elegante nella struttura. Ma non e sicuro. Secondo il Veracode GenAI Code Security Report 2025, che ha analizzato oltre 100 LLM su 80 task di codifica in Java, Python, C# e JavaScript, il 45% del codice AI-generated fallisce i security test e introduce vulnerabilità OWASP Top 10 nel codebase. Il 62% presenta design flaw, non bug ovvi ma errori architetturali profondi che emergono solo in produzione. E il codice prodotto da AI introduce 2.74 volte più vulnerabilità rispetto al codice scritto da sviluppatori umani.
Questi dati non sono una condanna del vibe coding. Sono un promemoria che l'AI cambia il chi scrive il codice, non la necessità di testarlo. Anzi, la amplifica. Quando un agente AI genera 500 righe di codice in 30 secondi, il bottleneck della produttività si sposta dalla scrittura alla verifica. Chi non ha una strategia di testing robusta per il codice AI si ritrova con una codebase la cui qualità e opaca, i cui bug sono imprevedibili e la cui sicurezza e una scommessa.
Questo articolo costruisce quella strategia. Dalla comprensione dei pattern di errore unici del codice AI, passando per unit test, property-based testing, static analysis, SAST e mutation testing, fino a una checklist operativa per decidere quando accettare o rifiutare il codice generato da un assistente. Il target sono sviluppatori che già usano AI tool nel loro workflow e vogliono costruire la fiducia necessaria per farlo in modo professionale.
Cosa Imparerai
- perchè il codice AI-generated fallisce in modi diversi dal codice umano
- Tipologie di bug specifiche: hallucinated API, logic error, security hole
- Unit testing e property-based testing con pytest e Hypothesis
- Static analysis con ESLint e SonarQube configurati per AI code
- Security testing con Semgrep: regole custom per pattern AI
- TDD con AI assistant: il ciclo Red-Green-Refactor diventa Red-AI-Green-Review
- Code review automatizzata tramite pipeline multi-agent
- Metriche di qualità: coverage, mutation score, cyclomatic complexity
- Checklist operativa per accettare o rifiutare codice AI
perchè il Codice AI Ha Bisogno di Testing Specifico
Il codice scritto da uno sviluppatore umano porta con se un modello mentale del sistema. Lo sviluppatore sa cosa c'è sopra e sotto il modulo che sta scrivendo, ha visto il codice girare in produzione, conosce le edge case che bruciano. L'AI non ha questo contesto. Opera su pattern statistici estratti da miliardi di righe di codice, e genera soluzioni che sono plausibili dato il prompt, non necessariamente corrette dato il sistema.
Questa differenza produce un profilo di errore fondamentalmente diverso. Il codice umano tende a fallire in modo prevedibile: un developer stanco copia il codice sbagliato, dimentica un null-check, usa la logica di business di ieri per il modello di oggi. Errori puntuali, tracciabili, spesso rivelati dai test esistenti. Il codice AI fallisce in modo strutturale: implementa l'API sbagliata con assoluta coerenza, introduce un pattern di vulnerabilità attraverso decine di funzioni, costruisce un'architettura che funziona perfettamente per il 95% dei casi e collassa sul restante 5% con comportamenti impossibili da anticipare senza una suite di test pensata per questo.
Il Problema della Confidenza Sintetica
L'AI genera codice con la stessa confidenza indipendentemente dalla correttezza. Un modello che hallucina una funzione inesistente di una libreria lo fa con la stessa sicurezza con cui implementa un algoritmo corretto. Non c'è segnale di incertezza nel testo prodotto. Questo rende il review visivo inaffidabile: il codice sembra giusto perchè e scritto bene. Solo l'esecuzione rivela la verita.
Tre caratteristiche specifiche del codice AI richiedono un approccio di testing diverso dal testing tradizionale:
- Varianza contestuale: Lo stesso prompt su codebase diversi produce codice con profili di qualità molto diversi. Il testing deve essere specifico al contesto di deployment, non solo alla funzionalità astratta.
- Coerenza interna degli errori: Se l'AI introduce un pattern sbagliato (per esempio, gestione non sicura dell'input utente), lo replica in tutte le funzioni simili. I bug non sono isolati: sono sistemici. Questo richiede test che cercano pattern, non solo casi singoli.
- Conoscenza datata: I modelli hanno knowledge cutoff. Usano API deprecate, pattern di sicurezza obsoleti, dipendenze con vulnerabilità note. Il testing deve includere un layer di verifica delle dipendenze e dei pattern di sicurezza attuali.
Tipologie di Bug nel Codice AI-Generated
Prima di costruire una strategia di testing efficace, e necessario sapere cosa si sta cercando. Il codice AI produce categorie di bug distinte, ognuna con caratteristiche proprie che richiedono approcci di rilevazione diversi.
1. Hallucinated API
Il bug più insidioso e anche il più documentato. L'AI inventa funzioni, metodi, parametri
e moduli che non esistono, ma con nomi cosi plausibili che passano il review visivo senza
difficolta. Un esempio classico: pandas.DataFrame.filter_by_threshold() non
esiste, ma suona esattamente come qualcosa che dovrebbe esistere. Il codice compila (se
non c'è type checking rigoroso), passa il linting, e fallisce a runtime con un
AttributeError che emerge solo quando la funzione viene chiamata con dati reali.
La difesa primaria contro le hallucinated API e l'esecuzione, non il review. Unit test che chiamano effettivamente il codice, anche con input minimi, rilevano immediatamente questo tipo di errore. Il type checking statico con mypy (Python) o TypeScript strict mode aggiunge un layer di protezione a compile time.
2. Logic Error e Off-by-One
Il codice AI e sorprendentemente bravo con la logica semplice e sorprendentemente fragile con la logica al boundary. Indici di array, range inclusivi vs esclusivi, condizioni di terminazione dei loop: questi sono i punti in cui il codice AI introduce errori sottili che i test standard non rilevano se non sono progettati specificamente per testare i valori limite.
3. Security Hole da Pattern Noti
Il Veracode Report 2025 documenta che gli LLM falliscono a difendere contro XSS nell'86% dei casi rilevanti e contro log injection nell'88% dei casi. Questi non sono bug casuali: sono conseguenza del fatto che il training data contiene enormi quantità di codice vulnerabile che il modello ha imparato a replicare. SQL injection, SSRF, path traversal, improper deserialization: pattern sistematici che richiedono SAST dedicato, non solo test funzionali.
4. Design Flaw Architetturale
Il più difficile da rilevare con i test automatici. L'AI costruisce soluzioni che funzionano localmente ma non scalano, che hanno le giuste interfacce ma le responsabilità sbagliate, che rispettano il contratto richiesto ma violano principi architetturali fondamentali (single responsibility, separation of concerns, loose coupling). Il 62% di design flaw citato da Veracode rientra in questa categoria. La rilevazione richiede un mix di metriche di complessità, analisi delle dipendenze e code review strutturata.
Framework di Testing per Codice AI
Unit Test Strategy: Testing dei Boundary
La strategia di unit testing per codice AI deve essere spostata verso i valori limite e i casi non ovvi. Il codice AI gestisce bene il caso happy path (e stato addestrato su milioni di esempi del caso happy path). Fallisce sui boundary, sugli input anomali, sulla gestione degli errori.
Una unit test suite efficace per codice AI include:
- Test con input al limite del range valido (valori zero, negativi, max-int)
- Test con input vuoti, null, None, stringa vuota
- Test con input che contengono caratteri speciali o encoding inatteso
- Test che verificano il comportamento in caso di errore (exception handling)
- Test che verificano i side effect (modifiche a stato condiviso, database, filesystem)
# test_ai_generated.py
# Strategia di testing per codice generato da AI
import pytest
from hypothesis import given, strategies as st, settings
from hypothesis import HealthCheck
# Funzione generata da AI (esempio)
# def process_user_input(data: dict) -> dict:
# return {
# "name": data["name"].strip().title(),
# "age": int(data["age"]),
# "email": data["email"].lower()
# }
from my_module import process_user_input
class TestAIGeneratedBoundaries:
"""Test dei valori limite per codice AI-generated."""
def test_nominal_case(self):
"""Happy path - il caso che l'AI ha quasi certamente gestito bene."""
result = process_user_input({
"name": "mario rossi",
"age": "30",
"email": "MARIO@EXAMPLE.COM"
})
assert result["name"] == "Mario Rossi"
assert result["age"] == 30
assert result["email"] == "mario@example.com"
def test_empty_name(self):
"""Edge case: nome vuoto - spesso non gestito dall'AI."""
with pytest.raises((ValueError, KeyError)):
process_user_input({"name": "", "age": "25", "email": "a@b.com"})
def test_negative_age(self):
"""Edge case: eta negativa - l'AI spesso non valida il range."""
with pytest.raises(ValueError, match="age must be positive"):
process_user_input({"name": "Test", "age": "-5", "email": "a@b.com"})
def test_missing_required_field(self):
"""Edge case: campo mancante - gestione KeyError o default?"""
with pytest.raises(KeyError):
process_user_input({"name": "Test", "age": "25"})
def test_sql_injection_in_name(self):
"""Security: input injection non deve passare invariato."""
malicious = "'; DROP TABLE users; --"
result = process_user_input({
"name": malicious,
"age": "25",
"email": "a@b.com"
})
# Il nome deve essere sanitizzato o rifiutato
assert "DROP TABLE" not in result.get("name", "")
def test_extremely_long_input(self):
"""Edge case: input molto lungo - buffer overflow / ReDoS."""
long_name = "a" * 10000
# Non deve appendere indefinitamente o crashare
with pytest.raises((ValueError, OverflowError)):
process_user_input({"name": long_name, "age": "25", "email": "a@b.com"})
def test_unicode_name(self):
"""Compatibilità Unicode: nomi non-ASCII."""
result = process_user_input({
"name": "giuseppe gallo",
"age": "28",
"email": "giuseppe@example.it"
})
assert result["name"] == "Giuseppe Gallo"
class TestAIGeneratedWithMocks:
"""Test con mock per isolare le dipendenze."""
def test_database_not_called_on_validation_error(self, mocker):
"""Verifica che il DB non venga chiamato se la validazione fallisce."""
mock_db = mocker.patch("my_module.database.save")
with pytest.raises(ValueError):
process_user_input({"name": "", "age": "25", "email": "a@b.com"})
mock_db.assert_not_called() # L'AI spesso chiama il DB prima di validare
Property-Based Testing con Hypothesis
Il property-based testing e lo strumento più potente disponibile per testare codice AI. Invece di definire singoli test case, si definiscono proprietà invarianti che devono valere per qualsiasi input nel dominio specificato, e Hypothesis genera automaticamente centinaia di input per trovare controesempi. Questa tecnica e particolarmente efficace contro il codice AI perchè esplora sistematicamente lo spazio degli input, incluse le edge case che non avremmo pensato di testare manualmente.
Un paper del 2025 pubblicato su arXiv (Agentic Property-Based Testing: Finding Bugs Across the Python Ecosystem) ha dimostrato che un agente Claude Code che genera test Hypothesis trova bug diversi e complementari rispetto ai test scritti manualmente, con pochi falsi positivi. L'approccio funziona perchè Hypothesis e costruito su Claude Code, conoscendo il dominio del testing.
# test_properties.py
# Property-based testing con Hypothesis per codice AI
import pytest
from hypothesis import given, strategies as st, settings, assume
from hypothesis import HealthCheck
from decimal import Decimal
# Funzione AI-generated: calcolo sconto
# def calculate_discount(price: float, discount_percent: float) -> float:
# if discount_percent < 0 or discount_percent > 100:
# raise ValueError("Discount must be between 0 and 100")
# return price * (1 - discount_percent / 100)
from pricing import calculate_discount
# Proprietà 1: idempotenza - sconto 0% non cambia il prezzo
@given(price=st.floats(min_value=0.01, max_value=1_000_000.0, allow_nan=False))
def test_zero_discount_preserves_price(price):
"""Sconto zero deve restituire il prezzo originale."""
result = calculate_discount(price, 0.0)
assert abs(result - price) < 0.01 # tolleranza floating point
# Proprietà 2: monotonia - sconto maggiore = prezzo minore
@given(
price=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False),
discount1=st.floats(min_value=0.0, max_value=50.0, allow_nan=False),
discount2=st.floats(min_value=50.0, max_value=100.0, allow_nan=False),
)
def test_higher_discount_lower_price(price, discount1, discount2):
"""Sconto maggiore deve produrre prezzo inferiore o uguale."""
result1 = calculate_discount(price, discount1)
result2 = calculate_discount(price, discount2)
assert result1 >= result2
# Proprietà 3: range output - prezzo finale sempre tra 0 e prezzo originale
@given(
price=st.floats(min_value=0.0, max_value=1_000_000.0, allow_nan=False),
discount=st.floats(min_value=0.0, max_value=100.0, allow_nan=False),
)
def test_output_range(price, discount):
"""Il prezzo scontato deve essere nel range [0, price]."""
result = calculate_discount(price, discount)
assert 0.0 <= result <= price + 0.01 # piccola tolleranza FP
# Proprietà 4: boundary - sconto 100% deve azzerare il prezzo
@given(price=st.floats(min_value=0.01, max_value=1_000_000.0, allow_nan=False))
def test_full_discount_zero_price(price):
"""Sconto del 100% deve risultare in prezzo zero."""
result = calculate_discount(price, 100.0)
assert abs(result) < 0.01
# Proprietà 5: validazione input - discount fuori range solleva eccezione
@given(discount=st.one_of(
st.floats(max_value=-0.001, allow_nan=False),
st.floats(min_value=100.001, allow_nan=False, allow_infinity=False)
))
def test_invalid_discount_raises(discount):
"""Discount fuori range [0, 100] deve sollevare ValueError."""
assume(not (discount != discount)) # esclude NaN
with pytest.raises(ValueError):
calculate_discount(100.0, discount)
# Test con st.composite per dati correlati
@st.composite
def valid_order(draw):
"""Strategia composita per ordini validi."""
price = draw(st.floats(min_value=1.0, max_value=10000.0, allow_nan=False))
discount = draw(st.floats(min_value=0.0, max_value=99.9, allow_nan=False))
return {"price": price, "discount": discount}
@given(order=valid_order())
@settings(max_examples=500, suppress_health_check=[HealthCheck.too_slow])
def test_order_processing_invariants(order):
"""Invarianti su ordini casuali generati da Hypothesis."""
result = calculate_discount(order["price"], order["discount"])
# Il risultato deve essere un numero finito
assert result == result # esclude NaN
assert result < float('inf')
# Il risultato deve essere non negativo
assert result >= 0.0
Static Analysis: ESLint, SonarQube e Regole Custom
L'analisi statica e il primo layer di difesa contro il codice AI di bassa qualità. Viene eseguita senza dover far girare il codice, e per questo e ideale da integrare nel ciclo di commit o nella CI/CD pipeline come gate obbligatorio prima di qualsiasi deploy.
Per il codice AI, la configurazione standard degli strumenti di static analysis non
basta. Il codice AI tende a produrre pattern specifici che richiede regole dedicate:
funzioni troppo lunghe generate in un unico blocco, dipendenze circolari, uso eccessivo
di any in TypeScript, importazioni di moduli inesistenti o deprecati.
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2024
},
"plugins": ["@typescript-eslint", "security", "sonarjs"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/strict",
"plugin:security/recommended",
"plugin:sonarjs/recommended"
],
"rules": {
// Regole critiche per codice AI-generated
// Proibisce 'any' esplicito - l'AI lo usa spesso per "sicurezza"
"@typescript-eslint/no-explicit-any": "error",
// Richiede return type esplicito - l'AI spesso lo omette
"@typescript-eslint/explicit-function-return-type": ["error", {
"allowExpressions": false,
"allowTypedFunctionExpressions": true
}],
// Cognitive complexity: l'AI genera funzioni lunghe e complesse
"sonarjs/cognitive-complexity": ["error", 15],
// Duplicazione: l'AI replica pattern identici in funzioni diverse
"sonarjs/no-duplicate-string": ["warn", 3],
"sonarjs/no-identical-functions": "error",
// Security: pattern comuni nel codice AI vulnerabile
"security/detect-object-injection": "error",
"security/detect-non-literal-regexp": "error",
"security/detect-possible-timing-attacks": "error",
"security/detect-unsafe-regex": "error",
// Complessità funzione - l'AI genera monoliti
"max-lines-per-function": ["warn", {
"max": 50,
"skipBlankLines": true,
"skipComments": true
}],
// Nesting profondo - pattern frequente nel codice AI
"max-depth": ["warn", 4],
// Promise handling - l'AI dimentica spesso await
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
// Null safety
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/strict-null-checks": "error",
// Import di moduli non utilizzati (hallucinated imports)
"@typescript-eslint/no-unused-vars": "error",
"no-unused-vars": "off" // sostituito da typescript version
},
"overrides": [
{
// Configurazione più stringente per codice generato da AI
"files": ["**/generated/**/*.ts", "**/ai-output/**/*.ts"],
"rules": {
"sonarjs/cognitive-complexity": ["error", 10],
"max-lines-per-function": ["error", {"max": 30}]
}
}
]
}
Per SonarQube, oltre alla configurazione standard, e utile configurare un Quality Gate specifico per AI code con soglie più stringenti: nessuna issue bloccante, coverage minima al 80%, duplicazione massima al 3%, e un limite esplicito sulla complessità ciclomatica delle funzioni.
Security Testing: SAST con Semgrep per Codice AI
Il SAST (Static Application Security Testing) specializzato per il codice AI va oltre il linting di stile: cerca pattern di vulnerabilità attivi, spesso invisibili a un review manuale perchè sintatticamente corretti. Semgrep e lo strumento di riferimento per questa categoria nel 2025, con 20.000+ regole predefinite e la possibilità di scrivere regole custom che matchano esattamente i pattern che l'AI introduce.
In novembre 2025, Semgrep ha annunciato la private beta del suo AI-powered detection system, che combina analisi statica tradizionale con reasoning contestuale per ridurre i falsi positivi del 91% rispetto ai SAST standalone. Per il codice AI, questo approccio ibrido e particolarmente efficace perchè capisce il contesto in cui un pattern viene usato, non solo la sua presenza.
# semgrep-ai-patterns.yaml
# Regole custom Semgrep per pattern di vulnerabilità tipici del codice AI-generated
rules:
# Regola 1: SQL injection via f-string o concatenazione
- id: ai-sql-injection-fstring
patterns:
- pattern: |
cursor.execute(f"...{$VAR}...")
- pattern: |
cursor.execute("..." + $VAR + "...")
message: |
Potenziale SQL injection via interpolazione diretta.
L'AI genera spesso query con f-string invece di parametri.
Usa cursor.execute("SELECT ... WHERE id = %s", (var,))
languages: [python]
severity: ERROR
metadata:
category: security
owasp: "A03:2021 - Injection"
ai-pattern: true
# Regola 2: Path traversal in file operations
- id: ai-path-traversal
patterns:
- pattern: |
open($BASE + $USER_INPUT, ...)
- pattern: |
open(os.path.join($BASE, $USER_INPUT), ...)
message: |
Potenziale path traversal. L'AI spesso concatena path utente
senza sanitizzazione. Usa pathlib.Path().resolve() e verifica
che il path risultante sia dentro la directory consentita.
languages: [python]
severity: ERROR
# Regola 3: Hardcoded credentials (pattern AI molto comune)
- id: ai-hardcoded-credentials
patterns:
- pattern: |
password = "$VALUE"
- pattern: |
api_key = "$VALUE"
- pattern: |
secret = "$VALUE"
- pattern: |
token = "$VALUE"
message: |
Credenziali hardcoded rilevate. L'AI inserisce spesso
placeholder realistici che sembrano credenziali reali.
Usa variabili d'ambiente o un secret manager.
languages: [python, javascript, typescript]
severity: ERROR
# Regola 4: Eval su input esterno
- id: ai-eval-injection
patterns:
- pattern: eval($USER_INPUT)
- pattern: exec($USER_INPUT)
message: |
eval/exec su input controllabile dall'utente. Pattern pericoloso
che l'AI introduce quando genera interpreti o sistemi dinamici.
languages: [python, javascript]
severity: ERROR
# Regola 5: Deserializzazione non sicura (Python pickle)
- id: ai-unsafe-deserialization
patterns:
- pattern: pickle.loads($DATA)
- pattern: pickle.load($FILE)
message: |
pickle.loads/load su dati non fidati. L'AI usa pickle
per semplicità senza considerare le implicazioni di sicurezza.
Usa json.loads() o librerie sicure come marshmallow.
languages: [python]
severity: ERROR
# Regola 6: JWT senza verifica firma
- id: ai-jwt-no-verification
patterns:
- pattern: |
jwt.decode($TOKEN, options={"verify_signature": False})
message: |
JWT decodificato senza verifica firma. Pattern frequente
nel codice AI generato per ambienti di sviluppo che poi
finisce in produzione.
languages: [python]
severity: ERROR
# Regola 7: Regex potenzialmente catastrofica (ReDoS)
- id: ai-catastrophic-regex
pattern-regex: '\(\.\*\)+|\(\.\+\)+'
message: |
Pattern regex potenzialmente vulnerabile a ReDoS.
L'AI genera spesso regex con backtracking esponenziale.
languages: [python, javascript, typescript]
severity: WARNING
#!/bin/bash
# scan-ai-code.sh
# Script per security scan di codice generato da AI
set -euo pipefail
AI_CODE_DIR="{{$1:-src}}"
RULES_FILE="semgrep-ai-patterns.yaml"
REPORT_FILE="security-report.json"
echo "Avvio security scan su: $AI_CODE_DIR"
# Scan con regole custom AI + ruleset OWASP
semgrep scan \
--config "$RULES_FILE" \
--config "p/owasp-top-ten" \
--config "p/python" \
--json \
--output "$REPORT_FILE" \
"$AI_CODE_DIR"
# Analizza risultati
ERRORS=$(jq '.results | map(select(.extra.severity == "ERROR")) | length' "$REPORT_FILE")
WARNINGS=$(jq '.results | map(select(.extra.severity == "WARNING")) | length' "$REPORT_FILE")
echo "Risultati: $ERRORS error(s), $WARNINGS warning(s)"
# Fail se ci sono error (non warning)
if [ "$ERRORS" -gt 0 ]; then
echo "FAIL: $ERRORS vulnerabilità critiche rilevate nel codice AI"
jq '.results[] | select(.extra.severity == "ERROR") | {file: .path, line: .start.line, message: .extra.message}' "$REPORT_FILE"
exit 1
fi
echo "Security scan completato con successo"
Dato Critico: Veracode 2025
Il report Veracode 2025 ha analizzato oltre 100 LLM su 80 task di codifica. Java e il linguaggio più a rischio con il 72% di security failure rate. Python, C# e JavaScript si attestano tra il 38% e il 45%. La cosa più allarmante: i modelli più recenti e avanzati non sono più sicuri di quelli vecchi. La sicurezza del codice AI non migliora con la dimensione del modello.
TDD con AI Assistant: Red-AI-Green-Review
Il Test-Driven Development tradizionale segue il ciclo Red-Green-Refactor: si scrive il test (che fallisce), si implementa il minimo necessario per farlo passare, si refactora. Con un AI assistant, questo ciclo si trasforma in Red-AI-Green-Review: si scrivono i test prima (mantenendo il controllo umano sulle specifiche), si delega all'AI l'implementazione, si verifica che tutti i test passino, si fa code review focalizzata sui pattern di sicurezza e architettura.
Questo approccio ha un vantaggio fondamentale: i test scritti dallo sviluppatore rappresentano le specifiche reali del sistema, non la comprensione dell'AI del prompt. Se l'AI genera codice che non passa i test, il problema e esplicito e misurabile. Se il codice passa tutti i test ma ha problemi di sicurezza, Semgrep li trova. Se ha design flaw, la code review li identifica. Il ciclo e completo.
# Passo 1: Lo sviluppatore scrive i test PRIMA
# test_payment_processor.py
import pytest
from decimal import Decimal
class TestPaymentProcessor:
"""
Specifiche per PaymentProcessor scritte PRIMA
di chiedere all'AI di implementarlo.
"""
def test_process_valid_payment(self, processor):
"""Pagamento valido deve restituire transaction_id."""
result = processor.process(
amount=Decimal("99.99"),
currency="EUR",
card_token="tok_valid_test"
)
assert result.success is True
assert result.transaction_id is not None
assert len(result.transaction_id) == 36 # UUID format
def test_negative_amount_rejected(self, processor):
"""Importo negativo deve sollevare ValueError."""
with pytest.raises(ValueError, match="Amount must be positive"):
processor.process(
amount=Decimal("-10.00"),
currency="EUR",
card_token="tok_valid_test"
)
def test_unsupported_currency_rejected(self, processor):
"""Valuta non supportata deve sollevare ValueError."""
with pytest.raises(ValueError, match="Currency not supported"):
processor.process(
amount=Decimal("50.00"),
currency="XYZ",
card_token="tok_valid_test"
)
def test_invalid_token_raises_payment_error(self, processor):
"""Token non valido deve sollevare PaymentError, non crashare."""
with pytest.raises(PaymentError, match="Invalid card token"):
processor.process(
amount=Decimal("50.00"),
currency="EUR",
card_token=""
)
def test_duplicate_idempotency(self, processor):
"""Stesso idempotency_key non deve creare duplicati."""
result1 = processor.process(
amount=Decimal("25.00"),
currency="EUR",
card_token="tok_valid_test",
idempotency_key="key-123"
)
result2 = processor.process(
amount=Decimal("25.00"),
currency="EUR",
card_token="tok_valid_test",
idempotency_key="key-123"
)
assert result1.transaction_id == result2.transaction_id
def test_amount_precision_preserved(self, processor):
"""Precisione decimale deve essere mantenuta."""
result = processor.process(
amount=Decimal("0.01"),
currency="EUR",
card_token="tok_valid_test"
)
assert result.charged_amount == Decimal("0.01")
# Passo 2: Si chiede ad AI di implementare PaymentProcessor
# Prompt: "Implementa PaymentProcessor che passa tutti questi test.
# Usa Decimal per gli importi, valida input,
# gestisci l'idempotency, non usare float."
# Passo 3: Si verifica che l'implementazione AI passi tutti i test
# pytest test_payment_processor.py -v
# Passo 4: Si esegue Semgrep sull'implementazione
# semgrep --config semgrep-ai-patterns.yaml payment_processor.py
# Passo 5: Code review focalizzata su sicurezza e architettura
Code Review Automatizzata: Pipeline Multi-Agent
Una pipeline di code review multi-agent per codice AI combina diversi specialisti che operano in parallelo su aspetti diversi della qualità: un agente di security review, uno di code style, uno di performance, uno di architettura. Il risultato e un report strutturato che lo sviluppatore usa come punto di partenza per la revisione umana, non come sostituto.
L'approccio non e nuovo (esistono strumenti come CodeClimate, DeepSource, Codacy da anni), ma nel 2025 la qualità dei modelli ha reso praticabile una pipeline completamente agentica. Claude Code, per esempio, può essere orchestrato in subagenti paralleli che analizzano il codice da angolazioni diverse e aggregano i risultati in un singolo report di review.
# ai_code_review_pipeline.py
# Pipeline multi-agent per review di codice AI-generated
import asyncio
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import List
@dataclass
class ReviewResult:
agent: str
severity: str # "critical", "high", "medium", "low", "info"
category: str
message: str
file: str
line: int | None = None
async def run_security_agent(file_path: str) -> List[ReviewResult]:
"""Agente security: esegue Semgrep + bandit."""
results = []
# Semgrep scan
proc = await asyncio.create_subprocess_exec(
"semgrep", "--config", "p/owasp-top-ten",
"--json", file_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, _ = await proc.communicate()
import json
data = json.loads(stdout)
for finding in data.get("results", []):
results.append(ReviewResult(
agent="security",
severity="critical" if finding["extra"]["severity"] == "ERROR" else "high",
category="security",
message=finding["extra"]["message"],
file=finding["path"],
line=finding["start"]["line"]
))
return results
async def run_complexity_agent(file_path: str) -> List[ReviewResult]:
"""Agente complessità: analizza cyclomatic complexity."""
results = []
proc = await asyncio.create_subprocess_exec(
"radon", "cc", "-s", "-n", "B", file_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, _ = await proc.communicate()
for line in stdout.decode().splitlines():
if line.strip():
# radon output: " M 15:4 method_name - B (7)"
parts = line.strip().split()
if len(parts) >= 5:
results.append(ReviewResult(
agent="complexity",
severity="medium" if "B" in parts else "high",
category="maintainability",
message=f"Funzione con alta complessità: {' '.join(parts)}",
file=file_path
))
return results
async def run_coverage_agent(test_file: str, source_file: str) -> List[ReviewResult]:
"""Agente coverage: verifica che i test coprano il codice AI."""
results = []
proc = await asyncio.create_subprocess_exec(
"pytest", test_file,
f"--cov={source_file}",
"--cov-report=json:coverage.json",
"--quiet",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
await proc.communicate()
import json
try:
with open("coverage.json") as f:
cov_data = json.load(f)
total = cov_data["totals"]["percent_covered"]
if total < 80.0:
results.append(ReviewResult(
agent="coverage",
severity="high",
category="testing",
message=f"Coverage insufficiente: {total:.1f}% (minimo 80%)",
file=source_file
))
except FileNotFoundError:
results.append(ReviewResult(
agent="coverage",
severity="critical",
category="testing",
message="Impossibile calcolare la coverage. Test mancanti?",
file=source_file
))
return results
async def review_ai_code(
source_file: str,
test_file: str | None = None
) -> dict:
"""Pipeline completa di review per codice AI-generated."""
tasks = [
run_security_agent(source_file),
run_complexity_agent(source_file),
]
if test_file:
tasks.append(run_coverage_agent(test_file, source_file))
# Esecuzione parallela di tutti gli agenti
all_results = await asyncio.gather(*tasks, return_exceptions=True)
findings: List[ReviewResult] = []
for result in all_results:
if isinstance(result, Exception):
print(f"Agente fallito: {result}")
else:
findings.extend(result)
# Aggregazione risultati
critical = [f for f in findings if f.severity == "critical"]
high = [f for f in findings if f.severity == "high"]
return {
"total_findings": len(findings),
"critical": len(critical),
"high": len(high),
"approve": len(critical) == 0 and len(high) == 0,
"findings": findings
}
# Uso
if __name__ == "__main__":
result = asyncio.run(review_ai_code(
source_file="payment_processor.py",
test_file="test_payment_processor.py"
))
print(f"Review completata: {result['total_findings']} findings")
print(f"Approvato: {result['approve']}")
if not result["approve"]:
print("\nISSUE CRITICHE:")
for f in result["findings"]:
if f.severity in ["critical", "high"]:
print(f" [{f.severity.upper()}] {f.file}:{f.line} - {f.message}")
Metriche di qualità per Codice AI
Il testing del codice AI richiede un set di metriche più ampio del semplice code coverage. La coverage misura quante righe di codice sono eseguite dai test, ma non misura la qualità di quei test. Un codice AI può avere 90% di coverage con test che non verificano nulla di rilevante. Le metriche supplementari necessarie sono:
Mutation Score
Il mutation testing e il modo più affidabile per misurare la qualità dei test, non solo
la loro quantità. Un mutation tester (come mutmut per Python o
Stryker per JavaScript/TypeScript) introduce sistematicamente piccole
modifiche nel codice (mutanti: cambia > in >=, inverte un
booleano, rimuove un return) e verifica che i test esistenti rilevano questi cambiamenti.
Se un mutante sopravvive ai test, i test non sono abbastanza granulari.
Per codice AI, il mutation score target minimo e 70% (Gold tier). Punteggi sotto il 50% indicano una suite di test inadeguata, indipendentemente dalla coverage percentuale.
# setup.cfg - Configurazione mutmut
[mutmut]
paths_to_mutate=src/
backup=False
runner=python -m pytest tests/ -x -q
tests_dir=tests/
dict_synonyms=
# Escludi file di configurazione e init
exclude=setup.py,conftest.py
# Esegui mutation testing
# mutmut run
# Visualizza risultati
# mutmut results
# Esempio di output:
# Mutant #1: SURVIVED
# src/payment_processor.py:45
# - if amount <= 0:
# + if amount < 0:
#
# Indica che il test per amount=0 non esiste!
# Aggiungere: assert raises ValueError for amount=0
# Script per CI/CD con soglia minima
#!/bin/bash
mutmut run --no-progress
KILLED=$(mutmut results | grep "Killed" | grep -o '[0-9]*' | head -1)
TOTAL=$(mutmut results | grep "Total" | grep -o '[0-9]*' | head -1)
if [ -z "$TOTAL" ] || [ "$TOTAL" -eq 0 ]; then
echo "Nessun mutante generato"
exit 0
fi
SCORE=$(echo "scale=2; $KILLED * 100 / $TOTAL" | bc)
echo "Mutation score: $SCORE%"
# Soglia minima 70% per codice AI
if (( $(echo "$SCORE < 70" | bc -l) )); then
echo "FAIL: Mutation score insufficiente per codice AI ($SCORE% < 70%)"
exit 1
fi
echo "OK: Mutation score accettabile ($SCORE%)"
Cyclomatic Complexity
La complessità ciclomatica misura il numero di percorsi linearmente indipendenti attraverso una funzione. Una funzione con complessità 1 ha un solo percorso possibile (nessun branch). Una funzione con complessità 20 ha 20 percorsi e richiede almeno 20 test case per una coverage completa dei branch. Il codice AI tende a generare funzioni con alta complessità perchè cerca di gestire tutti i casi in un'unica funzione monolitica.
I target raccomandati per codice AI nel 2025 sono: valore mediano < 10 per funzione, flag automatico > 15 per review obbligatoria, e rifiuto automatico > 25 come gate CI/CD. Questi valori sono più stringenti del codice umano perchè il codice AI ad alta complessità e meno prevedibile nella gestione degli errori rispetto a codice complesso scritto da un developer.
Best Practices: Checklist per Accettare Codice AI
La seguente checklist rappresenta il gate di qualità minimo prima di integrare codice AI-generated in un codebase di produzione. Non e una lista esaustiva, ma copre le categorie di errore più frequenti documentate nel 2025.
Checklist: Gate di qualità per Codice AI-Generated
Layer 1 - Automated (CI/CD Gate, bloccante)
- Tutti i test unitari passano (0 failure)
- Security scan Semgrep: 0 issue di severity ERROR
- ESLint/TypeScript: 0 errori (warning accettabili con giustificazione)
- Code coverage >= 80% sulle linee nuove
- Nessuna dipendenza con CVE noto (audit npm/pip)
- Nessuna credenziale hardcoded (git-secrets, detect-secrets)
Layer 2 - Quality Metrics (warning se non rispettati)
- Mutation score >= 70% sui moduli critici
- Cyclomatic complexity < 15 per ogni funzione
- Cognitive complexity < 20 per ogni funzione
- Nessuna funzione con più di 50 righe
- Duplicazione codice < 3%
Layer 3 - Manual Review (obbligatoria per modifiche rilevanti)
- Logica di business verificata contro le specifiche originali
- Gestione degli errori revisionate: no silent catch, no generic Exception
- Edge case documentati nei test: input vuoto, null, valori estremi
- Nessuna chiamata a API esterne senza timeout e retry logic
- State mutation isolata: nessun side effect non documentato
- Property-based test per funzioni con input numerici o testuali
Layer 4 - Architectural Review (per nuovi moduli o refactoring significativi)
- Single Responsibility rispettata (una ragione di cambiamento per classe)
- Dipendenze verso astrazioni, non implementazioni concrete
- Nessuna dipendenza circolare tra moduli
- Interfacce pubbliche stabili e ben documentate
Anti-Pattern: Cosa NON Fare con il Codice AI
- Mai fare merge senza eseguire i test: anche per "piccole correzioni". Il codice AI e deterministico ma non trasparente: una modifica al prompt cambia l'intera implementazione in modi non ovvi.
- Mai fidarsi del fatto che "funziona sul mio computer": il codice AI e particolarmente sensibile alle differenze di ambiente. Test in CI/CD su un container pulito sono obbligatori.
- Mai accettare codice AI su moduli di autenticazione senza SAST: auth e authorization sono le aree dove il codice AI e statisticamente più pericoloso. Il Veracode Report cita JWT senza verifica, session management errato, e privilege escalation tra i top-5 vulnerabilità AI.
- Non usare AI per generare test per il codice AI: i test devono essere scritti dallo sviluppatore. Se chiedi all'AI di scrivere i test per il suo stesso codice, otterrai test che verificano l'implementazione AI, non le specifiche del sistema.
Conclusioni: Il Testing come Abilitatore del Vibe Coding
L'obiettivo di questo articolo non e spaventare chi usa AI per scrivere codice. E l'opposto: costruire la fiducia necessaria per usarlo bene. Il 45% di security failure rate del codice AI non significa che il 45% del codice AI che arriva in produzione e insicuro. Significa che senza un processo di verifica strutturato, quella e la probabilità. Con il processo giusto, quella percentuale crolla drasticamente.
La combinazione di TDD (test scritti prima, non dopo), property-based testing con Hypothesis, SAST con Semgrep, static analysis con ESLint configurato per pattern AI, e mutation testing per verificare la qualità dei test, crea un sistema di salvaguardia che rende il vibe coding professionale. Non rallenta lo sviluppo: lo stabilizza.
Il prossimo articolo della serie esplora il Prompt Engineering per IDE e Code Generation: come scrivere prompt che producono codice migliore, più sicuro, più manutenibile, riducendo il lavoro di testing a valle.
Risorse e Link Utili
Serie Vibe Coding e Sviluppo Agentico
Tutta la Serie
- Vibe Coding: Il Paradigma che ha Cambiato il 2025
- Claude Code: Sviluppo Agentico da Terminale
- Workflow Agentici: Decomporre Problemi per AI
- Multi-Agent Coding: LangGraph, CrewAI e AutoGen
- Testare il Codice Generato da AI (questo articolo)
- Prompt Engineering per IDE e Code Generation
- Sicurezza nel Vibe Coding: Rischi e Mitigazioni
- Il Futuro dello Sviluppo Agentico nel 2026
Approfondimenti Correlati
- Cursor IDE: Il Primo IDE AI-First - Come Cursor gestisce il testing del codice AI
- OWASP Top 10 2025: Le Vulnerabilità Attuali - Il contesto delle vulnerabilità che l'AI introduce
- Claude per Code Review - Usare Claude come agente di review







