Test di integrazione e funzionali
Verifichi come i componenti collaborano realmente tra loro
Test di integrazione e funzionali è una lezione PHP Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento PHP Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso PHP Academy include 4 lezioni in totale.
Oltre i test unitari
I test unitari dimostrano che una classe funziona in isolamento, con tutto il resto simulato. Tuttavia, i mock possono ingannare: il database reale, il router del framework o il livello HTTP si comportano in modo diverso. I test di integrazione verificano due o più componenti reali collegati tra loro; i test funzionali esercitano l'applicazione dall'inizio alla fine attraverso il suo punto di ingresso pubblico (una richiesta HTTP o un comando da console). In questa lezione imparerà a strutturare e scrivere entrambi.
La piramide dei test
Una suite sana ha la forma di una piramide:
- Molti test unitari — veloci, isolati, eseguiti continuamente.
- Meno test di integrazione — database, coda e cache reali, quindi più lenti.
- Pochi test funzionali/E2E — stack completo, i più lenti e fragili.
Invertire questo schema (ottenendo un «cono gelato» composto soprattutto da test E2E) produce una suite lenta e instabile. Porti la coverage verso il livello più basso ogni volta che è possibile.
Integrazione: repository con un database reale
Un test di integrazione per un repository usa una connessione a un database reale, non un mock, per individuare errori SQL, dello schema e del mapping. Utilizzi un database di test dedicato (oppure SQLite in memoria, se rispecchia abbastanza fedelmente il vostro dialetto) e un PDO reale.
<?php
use PHPUnit\Framework\TestCase;
final class UserRepositoryTest extends TestCase
{
private \PDO $pdo;
protected function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
}
public function test_persists_and_finds_a_user(): void
{
$repo = new UserRepository($this->pdo);
$id = $repo->create('ada@example.com');
self::assertSame('ada@example.com', $repo->find($id)->email);
}
}
Isolamento: rollback della transazione
I test di integrazione non devono lasciare dati o stato nei test successivi. Il metodo più rapido consiste nell'avvolgere ogni test in una transazione ed eseguire il rollback in tearDown(): il database torna pulito senza doverlo popolare di nuovo. (Il troncamento è l'alternativa quando il codice in esame esegue il commit o usa DDL.)
<?php
protected function setUp(): void
{
$this->pdo = TestDb::connection();
$this->pdo->beginTransaction();
}
protected function tearDown(): void
{
$this->pdo->rollBack(); // undo everything this test did
}
Fixture e factory
I test hanno bisogno di dati. Eviti grandi dump SQL fragili; preferisca piccole factory che creino solo le righe necessarie al test, con valori predefiniti sensati da sovrascrivere caso per caso. In questo modo l'intento di ogni test resta esplicito e resiste meglio alla crescita dello schema.
<?php
final class UserFactory
{
public static function create(\PDO $pdo, array $overrides = []): int
{
$data = array_merge(['email' => 'user'.uniqid().'@test.dev'], $overrides);
$stmt = $pdo->prepare('INSERT INTO users (email) VALUES (:email)');
$stmt->execute($data);
return (int) $pdo->lastInsertId();
}
}
Funzionale: usare l'app come un client
Un test funzionale interagisce con il kernel/router reale dell'applicazione tramite una richiesta simulata e verifica la risposta: codice di stato, intestazioni e corpo. WebTestCase di Symfony e gli helper per i test HTTP di Laravel fanno esattamente questo senza avviare un vero server web. Di seguito è riportata la struttura per Symfony.
<?php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class HealthControllerTest extends WebTestCase
{
public function test_health_endpoint_returns_ok(): void
{
$client = static::createClient();
$client->request('GET', '/health');
self::assertResponseIsSuccessful(); // 2xx
self::assertJson($client->getResponse()->getContent());
}
}
Verificare la risposta
I test funzionali verificano il comportamento osservabile al confine dell'applicazione: stato, struttura JSON ed effetti collaterali nel database. Di seguito, una richiesta POST crea una risorsa; controlliamo sia la risposta HTTP sia che il record sia effettivamente finito nell'archivio, dimostrando che l'intero stack ha collaborato correttamente.
<?php
public function test_creating_a_user_persists_it(): void
{
$client = static::createClient();
$client->request('POST', '/users', server: [
'CONTENT_TYPE' => 'application/json',
], content: json_encode(['email' => 'ada@example.com']));
self::assertResponseStatusCodeSame(201);
$repo = static::getContainer()->get(UserRepository::class);
self::assertNotNull($repo->findByEmail('ada@example.com'));
}
Simulare selettivamente i confini
Anche i test funzionali non dovrebbero chiamare veri gestori di pagamento né inviare email reali. Sostituisca solo i servizi esterni più periferici: nel container di test li sostituisca con fake o implementazioni in memoria, mantenendo reali il proprio codice, il routing e il database. In questo modo conserva la fedeltà end-to-end senza chiamate di rete instabili.
<?php
public function test_checkout_charges_via_gateway(): void
{
$client = static::createClient();
// Replace the real gateway binding with an in-memory fake:
static::getContainer()->set(PaymentGateway::class, new FakePaymentGateway());
$client->request('POST', '/checkout', content: json_encode(['cart' => 1]));
self::assertResponseIsSuccessful();
}
Suite separate, velocità separate
Divida il file phpunit.xml in test suite, in modo che la suite unitaria veloce venga eseguita a ogni salvataggio e le suite di integrazione/funzionali più lente vengano eseguite su richiesta o in CI. Le raggruppi per directory e le selezioni con --testsuite.
<phpunit>
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
</phpunit>Il determinismo è più importante della coverage
Il modo più rapido per perdere fiducia in una suite è renderla instabile. Renda deterministici i test di integrazione/funzionali:
- Blocchi il tempo (iniettando un orologio) invece di chiamare
time(). - Inizializzi il generatore casuale; eviti asserzioni dipendenti dall'ordine su query non ordinate.
- Ripristini lo stato del database a ogni test (rollback della transazione o troncamento).
- Non dipenda mai dall'ordine di esecuzione dei test.
Allineare il database di test alla produzione
SQLite in memoria è veloce, ma le differenze tra dialetti (distinzione tra maiuscole e minuscole, funzioni JSON, applicazione delle foreign key e tipi) possono nascondere bug che emergono solo sul vostro motore reale. Per il codice che usa SQL specifico del motore, esegua i test di integrazione sullo stesso motore usato in produzione, comunemente in un container Docker usa e getta in CI, invece di usare un sostituto comodo.
<?php
// Read connection from environment so CI points at a real Postgres/MySQL container
$dsn = getenv('TEST_DATABASE_URL') ?: 'sqlite::memory:';
$pdo = new \PDO($dsn);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// In CI, TEST_DATABASE_URL targets the same engine as production.
Verifica rapida
Come dovrebbe trattare un test funzionale un gestore di pagamenti di terze parti?
Riepilogo
Ha imparato a testare insieme più componenti:
- I test di integrazione usano database e servizi reali per individuare ciò che i mock nascondono; li isolano con il rollback delle transazioni.
- I test funzionali interagiscono con il kernel reale tramite richieste simulate e verificano le risposte e gli effetti collaterali.
- Le factory creano dati minimi; simuli solo i confini esterni più periferici.
- Divida le suite in base alla velocità e mantenga deterministici i test più lenti.
Prossimo argomento: misurare la qualità dei test con il mutation testing.
Domande Frequenti
La lezione «Test di integrazione e funzionali» è gratuita?
Sì — il testo completo di «Test di integrazione e funzionali» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso PHP Academy, passa a CoddyKit PRO. Il corso PHP Academy include 4 lezioni in totale.
Cosa imparerò in «Test di integrazione e funzionali»?
Verifichi come i componenti collaborano realmente tra loro Eserciti PHP Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare PHP Academy?
Non è richiesta alcuna esperienza precedente. PHP Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «Test di integrazione e funzionali»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione PHP Academy?
Sì. Ogni lezione PHP Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Il flusso di lavoro dello sviluppo guidato dai test
- Mock e stub con Mockery
- Test di integrazione e funzionali
- Mutation testing con Infection