Integrations- und Funktionstests
Testen, wie Komponenten tatsächlich zusammenspielen
Integrations- und Funktionstests ist eine kostenlose PHP Academy-Lektion auf CoddyKit. Dies ist Lektion 3 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des PHP Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der PHP Academy-Kurs umfasst insgesamt 4 Lektionen.
Über den Unit-Test hinaus
Unit-Tests beweisen, dass eine Klasse isoliert funktioniert, wenn alles gemockt ist. Doch Mocks können täuschen – die echte Datenbank, der Router des Frameworks oder die HTTP-Schicht verhalten sich möglicherweise anders. Integrationstests überprüfen zwei oder mehr echte, miteinander verbundene Komponenten; Funktionstests führen die Anwendung über ihren öffentlichen Einstiegspunkt (eine HTTP-Anfrage, einen Konsolenbefehl) von Anfang bis Ende aus. Diese Lektion ordnet beide Testarten ein und zeigt, wie Sie sie schreiben.
Die Testpyramide
Eine gesunde Testsuite hat die Form einer Pyramide:
- Viele Unit-Tests – schnell, isoliert und ständig ausgeführt.
- Weniger Integrationstests – echte DB/Queue/Cache, langsamer.
- Wenige Funktionstests/E2E-Tests – vollständiger Stack, am langsamsten und am fehleranfälligsten.
Eine Umkehrung davon (eine „Eistüte“ aus überwiegend E2E-Tests) ergibt eine langsame, unzuverlässige Testsuite. Verlagern Sie die Abdeckung nach Möglichkeit weiter nach unten.
Integration: Repository gegen eine echte DB
Ein Integrationstest für ein Repository verwendet eine tatsächliche Datenbankverbindung – keinen Mock –, um Fehler in SQL, Schema und Mapping zu erkennen. Verwenden Sie eine eigene Testdatenbank (oder ein In-Memory-SQLite, das Ihrem Dialekt ausreichend nahekommt) und ein echtes PDO.
<?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);
}
}
Isolation: Transaktions-Rollback
Integrationstests dürfen keinen Zustand zwischen den Tests weitergeben. Das schnellste Muster besteht darin, jeden Test in eine Transaktion einzuschließen und diese in tearDown() zurückzurollen – die DB wird ohne erneutes Anlegen von Testdaten in einen sauberen Zustand versetzt. (Das Leeren der Tabellen ist die Ausweichlösung, wenn der getestete Code Transaktionen abschließt oder DDL verwendet.)
<?php
protected function setUp(): void
{
$this->pdo = TestDb::connection();
$this->pdo->beginTransaction();
}
protected function tearDown(): void
{
$this->pdo->rollBack(); // undo everything this test did
}
Fixtures und Factories
Tests benötigen Daten. Vermeiden Sie unflexible, riesige SQL-Dumps und bevorzugen Sie kleine Factories, die nur die von einem Test benötigten Zeilen erstellen, mit sinnvollen Standardwerten, die Sie pro Fall überschreiben. So bleibt die Absicht jedes Tests klar und Änderungen am Schema lassen sich besser verkraften.
<?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();
}
}
Funktional: Die Anwendung wie ein Client aufrufen
Ein Funktionstest steuert den echten Anwendungskern bzw. Router mit einer simulierten Anfrage und prüft die Antwort – Statuscode, Header und Inhalt. Symfony's WebTestCase und Laravels HTTP-Testhelfer tun genau das, ohne einen echten Webserver zu starten. Im Folgenden sehen Sie die Struktur für 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());
}
}
Die Antwort prüfen
Funktionstests prüfen beobachtbares Verhalten an der Grenze: Status, JSON-Struktur und Nebenwirkungen in der DB. Unten erstellt ein POST eine Ressource. Wir überprüfen sowohl die HTTP-Antwort als auch, ob der Datensatz tatsächlich im Speicher gelandet ist – damit ist bewiesen, dass der gesamte Stack zusammengearbeitet hat.
<?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'));
}
Die äußeren Grenzen gezielt faken
Auch Funktionstests sollten keine echten Zahlungsanbieter aufrufen oder echte E-Mails versenden. Ersetzen Sie nur die äußersten externen Dienste – tauschen Sie sie im Test-Container gegen Fakes bzw. In-Memory-Implementierungen aus –, während Ihr eigener Code, das Routing und die DB echt bleiben. So bewahren Sie die End-to-End-Treue, ohne unzuverlässige Netzwerkaufrufe zu erzeugen.
<?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();
}
Getrennte Suiten, getrennte Geschwindigkeiten
Teilen Sie Ihre phpunit.xml in Testsuites auf, damit die schnelle Unit-Suite bei jedem Speichern ausgeführt wird und die langsamen Integrations- bzw. Funktionstests bei Bedarf oder in CI laufen. Gruppieren Sie nach Verzeichnis und wählen Sie mit --testsuite aus.
<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>Determinismus schlägt Coverage
Der schnellste Weg, das Vertrauen in eine Testsuite zu verlieren, ist Unzuverlässigkeit. Machen Sie Integrations- und Funktionstests deterministisch:
- Frieren Sie die Zeit ein (injizieren Sie eine Uhr), statt
time()aufzurufen. - Initialisieren Sie Zufallswerte mit einem Seed und vermeiden Sie bei unsortierten Abfragen von der Reihenfolge abhängige Assertions.
- Setzen Sie den DB-Zustand nach jedem Test zurück (Transaktions-Rollback bzw. Leeren der Tabellen).
- Verlassen Sie sich niemals auf die Ausführungsreihenfolge der Tests.
Die Test-DB an die Produktion angleichen
SQLite im Speicher ist schnell, aber Unterschiede zwischen den Dialekten (Groß- und Kleinschreibung, JSON-Funktionen, Durchsetzung von Fremdschlüsseln und Datentypen) können Fehler verbergen, die erst auf Ihrer tatsächlichen Engine auftreten. Für Code mit enginespezifischem SQL führen Sie Integrationstests gegen dieselbe Engine wie in der Produktion aus – häufig in einem kurzlebigen Docker-Container in CI – statt gegen einen praktischen Ersatz.
<?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.
Schnelltest
Wie sollte ein Funktionstest mit einem Zahlungsanbieter eines Drittanbieters umgehen?
Zusammenfassung
Sie haben gelernt, Komponenten gemeinsam zu testen:
- Integrationstests verwenden echte DBs und Dienste, um aufzudecken, was Mocks verbergen; isolieren Sie sie mit einem Transaktions-Rollback.
- Funktionstests steuern den echten Anwendungskern über simulierte Anfragen und prüfen Antworten sowie Nebenwirkungen.
- Factories erstellen minimale Daten; faken Sie nur die äußersten externen Grenzen.
- Teilen Sie Suiten nach Geschwindigkeit auf und halten Sie langsamere Tests deterministisch.
Als Nächstes: Messen der Qualität von Tests mit Mutationstests.
Häufig gestellte Fragen
Ist die Lektion „Integrations- und Funktionstests“ kostenlos?
Ja — der vollständige Text von „Integrations- und Funktionstests“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des PHP Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der PHP Academy-Kurs umfasst insgesamt 4 Lektionen.
Was lerne ich in „Integrations- und Funktionstests“?
Testen, wie Komponenten tatsächlich zusammenspielen Du übst PHP Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.
Brauche ich Erfahrung, um PHP Academy zu starten?
Keine Vorkenntnisse erforderlich. PHP Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 3 von 4.
Wie lange dauert die Lektion „Integrations- und Funktionstests“?
Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.
Kann ich in dieser PHP Academy-Lektion Code schreiben und ausführen?
Ja. Jede PHP Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.
Alle Lektionen in diesem Kurs
- Der Test-Driven-Development-Workflow
- Mocking und Stubbing mit Mockery
- Integrations- und Funktionstests
- Mutation Testing mit Infection