Salta al contenuto principale

04 - Test Intelligence - Smart Test Generation e Mutation Testing

AI-powered test generation (Diffblue, GitHub Copilot for tests), mutation testing (PIT), coverage optimization, gap detection, ROI di test intelligence.

Cosa ne pensi di questo articolo?

04 - Test Intelligence - Smart Test Generation e Mutation Testing
Quality EngineeringFoto:SiddePexels

In Breve

AI-powered test generation (Diffblue, GitHub Copilot for tests), mutation testing (PIT), coverage optimization, gap detection, ROI di test intelligence.

Test Intelligence per Codice AI-Generated

Il testing tradizionale non è sufficiente per validare codice generato da AI. I test manuali coprono solo i casi d'uso espliciti, mentre i difetti dell'AI si nascondono nei casi limite, nelle race condition e nelle assunzioni implicite. La test intelligence rappresenta un approccio evoluto che combina generazione automatica di test, mutation testing, property-based testing e fuzzing per scoprire difetti che i test convenzionali non catturano.

In questo articolo esploreremo le tecniche avanzate di testing specifiche per codice AI-generated, con esempi pratici di implementazione e metriche per valutare l'efficacia della suite di test.

Cosa Imparerai

  • Come funziona la generazione intelligente di test per codice AI
  • Mutation testing: verificare che i test catturino realmente i bug
  • Property-based testing per scoprire casi limite sconosciuti
  • Tecniche di fuzzing per codice AI-generated
  • Coverage gap detection e strategie di rimedio
  • ROI del test intelligence rispetto al testing tradizionale

Smart Test Generation

La generazione intelligente di test va oltre il semplice scaffolding di test unitari. Analizza il codice sorgente, identifica i percorsi di esecuzione critici, le condizioni limite e i pattern di errore comuni per generare test che massimizzano la probabilità di trovare difetti reali, non solo di raggiungere una percentuale di coverage.


# Framework di smart test generation per codice AI
import ast
import inspect
from typing import List, Dict, Any

class SmartTestGenerator:
    """Genera test intelligenti analizzando il codice sorgente"""

    def __init__(self, target_function):
        self.func = target_function
        self.source = inspect.getsource(target_function)
        self.tree = ast.parse(self.source)
        self.test_cases = []

    def generate_tests(self) -> List[Dict[str, Any]]:
        """Genera test cases basati sull'analisi del codice"""
        self._generate_happy_path_tests()
        self._generate_boundary_tests()
        self._generate_error_path_tests()
        self._generate_null_tests()
        self._generate_type_confusion_tests()
        return self.test_cases

    def _generate_boundary_tests(self):
        """Genera test per i valori limite identificati nel codice"""
        comparisons = self._extract_comparisons()
        for comp in comparisons:
            # Per ogni confronto numerico, testa: valore-1, valore, valore+1
            if isinstance(comp["value"], (int, float)):
                val = comp["value"]
                self.test_cases.extend([
                    {"type": "boundary", "input": val - 1,
                     "description": f"Just below boundary {val}"},
                    {"type": "boundary", "input": val,
                     "description": f"At boundary {val}"},
                    {"type": "boundary", "input": val + 1,
                     "description": f"Just above boundary {val}"},
                ])

    def _generate_error_path_tests(self):
        """Genera test per ogni handler di eccezione nel codice"""
        for node in ast.walk(self.tree):
            if isinstance(node, ast.ExceptHandler):
                exception_type = getattr(node.type, 'id', 'Exception')
                self.test_cases.append({
                    "type": "error_path",
                    "trigger": exception_type,
                    "description": f"Test error path: {exception_type}"
                })

    def _generate_null_tests(self):
        """Genera test con input None/null per ogni parametro"""
        sig = inspect.signature(self.func)
        for param_name in sig.parameters:
            if param_name == 'self':
                continue
            self.test_cases.append({
                "type": "null_input",
                "param": param_name,
                "input": None,
                "description": f"None input for {param_name}"
            })

    def _extract_comparisons(self):
        """Estrae i valori di confronto dal codice"""
        comparisons = []
        for node in ast.walk(self.tree):
            if isinstance(node, ast.Compare):
                for comparator in node.comparators:
                    if isinstance(comparator, ast.Constant):
                        comparisons.append({
                            "value": comparator.value,
                            "line": node.lineno
                        })
        return comparisons
      

Mutation Testing: Verificare l'Efficacia dei Test

Il mutation testing è la tecnica più potente per valutare la qualità di una suite di test. Funziona introducendo piccole modifiche deliberate (mutazioni) nel codice sorgente e verificando che almeno un test fallisca per ogni mutazione. Se una mutazione sopravvive (nessun test fallisce), significa che la suite di test ha un gap.

Per il codice AI-generated, il mutation testing è particolarmente importante perchè rivela se i test stanno realmente verificando la logica di business o solo eseguendo il codice senza asserzioni significative, un pattern comune quando anche i test sono generati dall'AI.


# Mutation testing framework per codice AI
class MutationTester:
    """Applica mutazioni al codice e verifica che i test le catturino"""

    MUTATION_OPERATORS = [
        ("arithmetic", lambda: [
            ("+", "-"), ("-", "+"), ("*", "/"), ("/", "*")
        ]),
        ("comparison", lambda: [
            ("==", "!="), ("!=", "=="), ("<", ">="),
            (">", "<="), ("<=", ">"), (">=", "<")
        ]),
        ("logical", lambda: [
            ("and", "or"), ("or", "and"), ("True", "False"),
            ("False", "True")
        ]),
        ("boundary", lambda: [
            ("< ", "<= "), ("<= ", "< "),
            ("> ", ">= "), (">= ", "> ")
        ]),
    ]

    def __init__(self, source_code, test_suite):
        self.source = source_code
        self.tests = test_suite
        self.results = []

    def run_mutations(self):
        """Esegue tutte le mutazioni e calcola il mutation score"""
        total_mutations = 0
        killed_mutations = 0

        for category, operators_fn in self.MUTATION_OPERATORS:
            for original, mutated in operators_fn():
                if original in self.source:
                    total_mutations += 1
                    mutated_code = self.source.replace(original, mutated, 1)
                    if self._test_detects_mutation(mutated_code):
                        killed_mutations += 1
                        status = "KILLED"
                    else:
                        status = "SURVIVED"

                    self.results.append({
                        "category": category,
                        "original": original,
                        "mutated": mutated,
                        "status": status
                    })

        mutation_score = (killed_mutations / total_mutations * 100
                         if total_mutations > 0 else 0)
        return {
            "total_mutations": total_mutations,
            "killed": killed_mutations,
            "survived": total_mutations - killed_mutations,
            "mutation_score": round(mutation_score, 2),
            "details": self.results
        }

    def _test_detects_mutation(self, mutated_code):
        """Verifica se almeno un test fallisce con il codice mutato"""
        # Esecuzione dei test con il codice mutato
        # Ritorna True se almeno un test fallisce (mutazione catturata)
        pass  # Implementazione dipendente dal test runner
      

Interpretazione del Mutation Score

Mutation Score qualità Test Azione per Codice AI
90-100% Eccellente Suite di test affidabile, merge consentito
75-89% Buona Verificare le mutazioni sopravvissute
50-74% Insufficiente Test aggiuntivi obbligatori prima del merge
0-49% Critica Suite di test da riscrivere, merge bloccato

Property-Based Testing

Il property-based testing è una tecnica che definisce proprietà invarianti del codice e genera automaticamente centinaia o migliaia di input per verificarle. A differenza dei test unitari classici che verificano casi specifici, il property-based testing esplora lo spazio degli input in modo sistematico, scoprendo casi limite che nessuno sviluppatore avrebbe pensato di testare.

Per il codice AI-generated, questa tecnica e particolarmente efficace perchè i difetti dell'AI si manifestano spesso con input inusuali che il developer non considera nei test manuali.


# Property-based testing con Hypothesis
from hypothesis import given, strategies as st, assume, settings

# Esempio: testare una funzione di calcolo sconto generata dall'AI
# La funzione AI potrebbe avere bug sui casi limite

@given(
    price=st.floats(min_value=0.01, max_value=100000),
    discount=st.floats(min_value=0, max_value=100)
)
@settings(max_examples=1000)
def test_discount_properties(price, discount):
    """Proprietà invarianti del calcolo sconto"""
    result = calculate_discount(price, discount)

    # Proprietà 1: il risultato non è mai negativo
    assert result >= 0, f"Negative result: {result}"

    # Proprietà 2: il risultato non supera mai il prezzo originale
    assert result <= price, f"Result {result} > price {price}"

    # Proprietà 3: sconto 0% non modifica il prezzo
    if discount == 0:
        assert result == price

    # Proprietà 4: sconto 100% porta a zero
    if discount == 100:
        assert result == 0

    # Proprietà 5: la funzione è monotona rispetto allo sconto
    # (più sconto = prezzo minore)

@given(
    items=st.lists(st.integers(min_value=1, max_value=1000),
                   min_size=0, max_size=100)
)
def test_sort_properties(items):
    """Proprietà invarianti di un algoritmo di ordinamento AI"""
    sorted_items = ai_sort(items)

    # Proprietà 1: stessa lunghezza
    assert len(sorted_items) == len(items)

    # Proprietà 2: stessi elementi (permutazione)
    assert sorted(sorted_items) == sorted(items)

    # Proprietà 3: ordinamento effettivo
    for i in range(len(sorted_items) - 1):
        assert sorted_items[i] <= sorted_items[i + 1]
      

Fuzzing per Codice AI

Il fuzzing genera input casuali o semi-casuali per scoprire crash, memory leak, eccezioni non gestite e comportamenti indefiniti. Per il codice AI-generated, il fuzzing e particolarmente utile per testare la robustezza dell'input validation, che l'AI spesso trascura.

Il fuzzing guidato dalla coverage (coverage-guided fuzzing) è l'approccio più efficace: utilizza feedback dalla coverage per generare input che esplorano nuovi percorsi di esecuzione, massimizzando la probabilità di trovare bug nascosti in branch poco visitati.


# Fuzzing framework per API generate dall'AI
import random
import string
import json

class APIFuzzer:
    """Fuzzer per endpoint API generati da AI"""

    def __init__(self, endpoint_spec):
        self.spec = endpoint_spec
        self.findings = []

    def fuzz_parameter(self, param_type, iterations=100):
        """Genera input fuzzati per un tipo di parametro"""
        generators = {
            "string": self._fuzz_strings,
            "integer": self._fuzz_integers,
            "email": self._fuzz_emails,
            "json": self._fuzz_json,
        }
        generator = generators.get(param_type, self._fuzz_strings)
        return [generator() for _ in range(iterations)]

    def _fuzz_strings(self):
        """Genera stringhe di test problematiche"""
        cases = [
            "",                          # empty
            " " * 1000,                  # spaces
            "A" * 100000,                # very long
            "\x00\x01\x02",             # null bytes
            "", # XSS
            "'; DROP TABLE users; --",   # SQL injection
            "../../../etc/passwd",       # path traversal
            "{{7*7}}",       # template injection
            "\r\n\r\nHTTP/1.1 200",     # header injection
            json.dumps({"$gt": ""}),   # NoSQL injection
        ]
        return random.choice(cases)

    def _fuzz_integers(self):
        """Genera interi ai limiti"""
        cases = [0, -1, 1, 2**31-1, -2**31, 2**63-1, -2**63]
        return random.choice(cases)

    def _fuzz_emails(self):
        """Genera email malformate"""
        cases = [
            "", "notanemail", "@nodomain", "user@",
            "a" * 500 + "@test.com",
            "user@" + "a" * 500 + ".com",
            "user+tag@domain.com",
            "user@[127.0.0.1]",
        ]
        return random.choice(cases)

    def run_fuzzing_campaign(self, send_request_fn):
        """Esegue campagna di fuzzing completa"""
        for param in self.spec["parameters"]:
            fuzzed_inputs = self.fuzz_parameter(param["type"])
            for fuzz_input in fuzzed_inputs:
                try:
                    response = send_request_fn(param["name"], fuzz_input)
                    if response.status_code == 500:
                        self.findings.append({
                            "param": param["name"],
                            "input": repr(fuzz_input),
                            "status": response.status_code,
                            "severity": "HIGH"
                        })
                except Exception as e:
                    self.findings.append({
                        "param": param["name"],
                        "input": repr(fuzz_input),
                        "error": str(e),
                        "severity": "CRITICAL"
                    })
        return self.findings
      

Coverage Gap Detection

Il coverage gap detection identifica le aree del codice AI che non sono coperte da test, con particolare attenzione ai percorsi critici: error handling, validazione degli input, condizioni di contorno e path di sicurezza. Non si tratta solo di aumentare la percentuale di coverage, ma di coprire strategicamente le aree a più alto rischio.

Priorità di Coverage per Codice AI

  • Priorità 1: Error handling e exception path - il gap più critico nell'AI code
  • Priorità 2: Input validation e sanitization - spesso assenti nel codice generato
  • Priorità 3: Boundary conditions - valori limite che l'AI non testa
  • Priorità 4: Security-critical paths - autenticazione, autorizzazione, cifratura
  • Priorità 5: Integration points - chiamate a servizi esterni, database, file system

ROI del Test Intelligence

Il test intelligence richiede un investimento iniziale maggiore rispetto al testing tradizionale, ma il ritorno è significativo. Il mutation testing rivela gap nascosti che causerebbero bug in produzione. Il property-based testing scopre categorie intere di difetti con un singolo test. Il fuzzing trova vulnerabilità che nessun test manuale intercetterebbe.

Confronto ROI: Testing Tradizionale vs Test Intelligence

Aspetto Testing Tradizionale Test Intelligence
Setup iniziale Basso Medio
Costo per test Alto (manuale) Basso (automatico)
Bug scoperti per ora 0.5-2 5-15
Copertura casi limite Scarsa Eccellente
Scalabilità Lineare Esponenziale

Integrazione nella Pipeline CI/CD

Il test intelligence deve essere integrato nella pipeline CI/CD per fornire feedback automatico sulla qualità del codice AI. I test property-based e il mutation testing possono essere eseguiti su ogni pull request, mentre le campagne di fuzzing più lunghe possono essere schedulate durante la notte o nel weekend.

Conclusioni

Il test intelligence rappresenta un salto qualitativo nella validazione del codice AI-generated. La generazione intelligente di test, il mutation testing, il property-based testing e il fuzzing formano un arsenale completo per scoprire difetti che i test tradizionali non catturano.

Nel prossimo articolo esploreremo i workflow di validazione umana: come strutturare i processi di code review, approvazione e pair programming con l'AI per garantire che il codice generato soddisfi gli standard di qualità del team.

La qualità dei test determina la qualità del software. E per il codice AI-generated, servono test più intelligenti, non solo più numerosi.

Domande frequenti

Qual è l'obiettivo principale della Test Intelligence nel contesto del codice generato da AI?

L'obiettivo principale della Test Intelligence è quello di scoprire difetti che i test convenzionali non catturano, come casi limite, race condition e assunzioni implicite.

Qual è la differenza tra il testing tradizionale e la generazione intelligente di test?

La generazione intelligente di test analizza il codice sorgente per identificare percorsi di esecuzione critici, condizioni limite e pattern di errore comuni, mentre il testing tradizionale copre solo i casi d'uso espliciti.

Qual è l'importanza del mutation testing nel contesto del codice AI-generated?

Il mutation testing è particolarmente importante per il codice AI-generated perché rivela se i test stanno realmente verificando la logica di business o solo eseguendo il codice senza asserzioni significative.

Qual è l'obiettivo del fuzzing nel contesto del codice AI-generated?

L'obiettivo del fuzzing è quello di scoprire crash, memory leak, eccezioni non gestite e comportamenti indefiniti generando input casuali o semi-casuali.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti