Introduzione ai test automatici su codebase PHP legacy: come iniziare senza riscrivere tutto
A ottobre 2024, dopo aver messo sotto Git il gestionale PHP legacy di un cliente piemontese - la storia la racconto nell'articolo sull'implementazione di Git su sistemi PHP legacy - il passo successivo era inevitabile: ogni volta che il collaboratore interno faceva una modifica, qualcos'altro si rompeva in un punto apparentemente non correlato. Un fix al calcolo dell'IVA rompeva la stampa delle bolle. Una modifica al filtro di ricerca prodotti faceva sparire i prezzi dal listino. Il codice era un monolite di 23.000 righe distribuite su 147 file senza nessuna separazione tra logica, presentazione e accesso ai dati - tutto nello stesso file PHP, con mysql_query inframmezzato a tag HTML e echo di JavaScript.
Il titolare mi ha chiesto: "possiamo fare qualcosa per evitare che ogni modifica rompa qualcos'altro?" La risposta è sì, e non richiede di riscrivere l'applicazione da zero. La tecnica si chiama characterization testing - un termine coniato da Michael Feathers nel libro Working Effectively with Legacy Code - e consiste nel catturare il comportamento attuale dell'applicazione in test automatici. Non testi cosa dovrebbe fare il codice (perché spesso non lo sai), ma cosa fa effettivamente. Quei test diventano la rete di sicurezza: se una modifica cambia il comportamento catturato, il test fallisce e sai che hai rotto qualcosa prima che arrivi in produzione.
In una settimana ho introdotto 34 test di caratterizzazione sugli endpoint critici del gestionale - login, ricerca prodotti, inserimento ordine, calcolo totali, stampa bolla - senza modificare una singola riga del codice applicativo. Nel primo mese dopo l'introduzione dei test, il tasso di bug in produzione è sceso del 70%. In questo articolo ti racconto come.
Stai cercando un Consulente Informatico esperto per introdurre test automatici sul tuo codebase PHP legacy? Nel mio profilo professionale trovi l'esperienza concreta su testing, refactoring e modernizzazione di applicazioni PHP 5.x-8.x. Contattami per una consulenza diretta.
Cosa sono i characterization test e perché funzionano su codice legacy?
Un characterization test - chiamato anche golden master test o approval test - non verifica una specifica corretta (come un unit test classico). Cattura l'output attuale del sistema per un dato input e lo salva come "risposta attesa". Da quel momento, se l'output cambia, il test fallisce. Non stai dicendo "il calcolo dell'IVA deve restituire 22%" - stai dicendo "con questi dati di input, il sistema attualmente restituisce questo output, e se l'output cambia dopo la mia modifica, voglio saperlo".
Questo approccio funziona su codice legacy perché non richiede di capire il codice internamente. Non devi sapere come funziona il calcolo dell'IVA per testarlo - devi solo sapere che con il prodotto X e la quantità Y il totale è Z. Se dopo una modifica il totale diventa W, il test te lo dice prima che il cliente se ne accorga.
La differenza rispetto ai test classici è fondamentale: i test classici richiedono una specifica chiara e codice testabile (classi con dipendenze iniettabili, separazione delle responsabilità). Il codice legacy non ha né l'una né l'altro. I characterization test accettano il codice com'è e lo fotografano.
Il setup: PHPUnit su un progetto PHP legacy senza framework
Sul gestionale piemontese - PHP 5.6, nessun Composer, nessun autoloader, nessun framework - il primo passo è stato introdurre Composer e PHPUnit come dipendenza di sviluppo:
# Installare Composer (se non presente)
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
# Inizializzare il progetto
cd /var/www/html
composer init --name="azienda/gestionale" --no-interaction
composer require --dev phpunit/phpunit
# Creare la struttura di test
mkdir -p tests/FeatureLa configurazione phpunit.xml per un progetto legacy senza autoloader PSR-4:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_NAME" value="gestionale_test"/>
</php>
</phpunit>Il database di test è una copia del database di produzione - stessa struttura, dati anonimizzati - perché i characterization test hanno bisogno di dati realistici per catturare il comportamento reale del sistema.
I primi smoke test: HTTP endpoint come scatole nere
Il modo più rapido per testare un'applicazione PHP legacy è trattarla come una scatola nera: invii una richiesta HTTP e verifichi la risposta. Non serve capire il codice interno - serve solo sapere che l'endpoint risponde e che l'output è stabile.
<?php
// tests/Feature/SmokeTest.php
use PHPUnit\Framework\TestCase;
class SmokeTest extends TestCase
{
private string $baseUrl = 'http://127.0.0.1';
/** @test */
public function la_homepage_risponde_con_200(): void
{
$ch = curl_init("{$this->baseUrl}/index.php");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->assertEquals(200, $code);
$this->assertStringContainsString('Gestionale', $body);
}
/** @test */
public function la_pagina_login_mostra_il_form(): void
{
$ch = curl_init("{$this->baseUrl}/login.php");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->assertEquals(200, $code);
$this->assertStringContainsString('name="username"', $body);
$this->assertStringContainsString('name="password"', $body);
}
/** @test */
public function la_ricerca_prodotti_restituisce_risultati(): void
{
// Simulare un utente loggato con cookie di sessione
$ch = curl_init("{$this->baseUrl}/search.php?q=valvola&cat=3");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIE, "PHPSESSID=test_session_id");
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->assertEquals(200, $code);
// Il golden master: verifica che il numero di risultati sia stabile
$this->assertStringContainsString('Trovati', $body);
}
}Questi test sono brutalmente semplici - e proprio per questo funzionano. Non verificano la logica interna, ma catturano il comportamento visibile. Se una modifica al calcolo dell'IVA rompe la ricerca prodotti (perché nel codice legacy tutto è accoppiato a tutto), il test sulla ricerca fallisce e lo scopri prima del deploy.
Il golden master: catturare output complessi
Per funzionalità con output complesso - come la stampa di una bolla o il calcolo di un preventivo - uso la tecnica del golden master: salvo l'output completo in un file di riferimento e confronto le esecuzioni successive con quel file.
<?php
// tests/Feature/GoldenMasterTest.php
class GoldenMasterTest extends TestCase
{
/** @test */
public function il_calcolo_totale_ordine_produce_output_stabile(): void
{
// Input fisso: ordine con 3 prodotti noti
$orderId = 42; // ordine di test con dati fissi nel DB di test
$output = $this->callEndpoint("/api/order_total.php?id={$orderId}");
$goldenFile = __DIR__ . '/golden/order_total_42.json';
if (!file_exists($goldenFile)) {
// Prima esecuzione: salva il golden master
file_put_contents($goldenFile, $output);
$this->markTestIncomplete('Golden master creato. Verificare manualmente e ri-eseguire.');
return;
}
// Esecuzioni successive: confronta con il golden master
$expected = file_get_contents($goldenFile);
$this->assertEquals($expected, $output, 'Output cambiato rispetto al golden master');
}
}La prima esecuzione crea il golden master - lo sviluppatore deve verificare manualmente che l'output sia corretto e approvarlo. Da quel momento, qualsiasi cambiamento nell'output viene rilevato automaticamente. Se il cambiamento è intenzionale (perché hai corretto un bug), aggiorni il golden master. Se non è intenzionale, hai appena catturato un bug prima che arrivi in produzione.
Ho descritto tecniche più avanzate - smoke test con harness HTTP, snapshot testing e test di caratterizzazione su database - nell'articolo sull'introduzione di test minimi su PHP legacy.
CI gate: i test che bloccano il deploy
I test hanno valore solo se vengono eseguiti. Il pre-commit hook che installo assicura che nessun commit passi senza test verdi:
#!/bin/bash
# .git/hooks/pre-commit
echo "Esecuzione test..."
vendor/bin/phpunit --colors=never 2>&1
if [ $? -ne 0 ]; then
echo "TEST FALLITI - commit bloccato"
exit 1
fiSul gestionale piemontese, questo hook ha bloccato 12 commit nel primo mese - 12 modifiche che avrebbero rotto qualcosa in produzione e che invece sono state corrette prima di arrivare sul server. Dodici potenziali incidenti evitati, dodici volte in cui il collaboratore ha detto "meno male che il test mi ha fermato".
Il passo successivo - test in CI con GitHub Actions, analisi di copertura, e test di integrazione con database reale - è descritto nell'articolo sul refactoring di codice PHP legacy. Ma i characterization test con il pre-commit hook sono il minimo indispensabile che installo su ogni progetto legacy - meno di una settimana di lavoro per una riduzione misurabile dei bug in produzione.
Se il tuo codebase PHP legacy non ha test e ogni modifica è una scommessa, il costo di introdurre i primi 20-30 characterization test - una settimana di lavoro - si ripaga al primo bug evitato. Contattami e costruiamo la rete di sicurezza per il tuo progetto.