Testy integracyjne i funkcjonalne
Testuj, jak komponenty rzeczywiście współpracują
Testy integracyjne i funkcjonalne to bezpłatna lekcja PHP Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej PHP Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs PHP Academy zawiera 4 lekcji w sumie.
Poza testy jednostkowe
Testy jednostkowe dowodzą, że klasa działa w izolacji, gdy wszystko jest zastąpione mockami. Jednak mocki mogą wprowadzać w błąd — rzeczywista baza danych, router frameworka czy warstwa HTTP działają inaczej. Testy integracyjne weryfikują współdziałanie co najmniej dwóch rzeczywistych komponentów; testy funkcjonalne sprawdzają aplikację od początku do końca za pośrednictwem jej publicznego punktu wejścia (żądania HTTP lub polecenia konsoli). W tej lekcji określimy zastosowanie obu rodzajów testów i je napiszemy.
Piramida testów
Zdrowy zestaw testów ma kształt piramidy:
- Wiele testów jednostkowych — szybkie, izolowane, uruchamiane stale.
- Mniej testów integracyjnych — korzystają z rzeczywistej bazy danych, kolejki i pamięci podręcznej, więc są wolniejsze.
- Niewiele testów funkcjonalnych/E2E — obejmują cały stos, są najwolniejsze i najbardziej podatne na awarie.
Odwrócenie tej proporcji — „rożek lodowy” z przewagą testów E2E — prowadzi do powstania wolnego i niestabilnego zestawu testów. W miarę możliwości należy przenosić pokrycie do niższych warstw.
Integracja: repozytorium z rzeczywistą bazą danych
Test integracyjny repozytorium korzysta z rzeczywistego połączenia z bazą danych — nie z mocka — aby wykrywać błędy w SQL, schemacie i mapowaniu. Należy używać dedykowanej testowej bazy danych (lub działającej w pamięci bazy SQLite, która jest wystarczająco zgodna z używanym dialektem) oraz rzeczywistego 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);
}
}
Izolacja: wycofywanie transakcji
Testy integracyjne nie mogą przenosić stanu między sobą. Najszybszy wzorzec polega na opakowaniu każdego testu w transakcję i wycofaniu jej w tearDown() — baza danych wraca do czystego stanu bez ponownego zasilania danymi. (Czyszczenie tabel jest rozwiązaniem zastępczym, gdy testowany kod zatwierdza transakcję lub korzysta z 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
}
Dane testowe i fabryki
Testy potrzebują danych. Należy unikać kruchych, ogromnych zrzutów SQL i preferować małe fabryki, które tworzą tylko wiersze potrzebne danemu testowi, z rozsądnymi wartościami domyślnymi nadpisywanymi w poszczególnych przypadkach. Dzięki temu intencja każdego testu jest jasna, a testy są odporne na rozbudowę schematu.
<?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();
}
}
Test funkcjonalny: aplikacja widziana oczami klienta
Test funkcjonalny steruje rzeczywistym jądrem/routerem aplikacji za pomocą symulowanego żądania i sprawdza odpowiedź — kod statusu, nagłówki oraz treść. WebTestCase w Symfony i pomocniki testów HTTP w Laravelu robią dokładnie to, bez uruchamiania rzeczywistego serwera WWW. Poniżej przedstawiono wariant dla 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());
}
}
Sprawdzanie odpowiedzi
Testy funkcjonalne sprawdzają obserwowalne zachowanie na granicy systemu: status, strukturę JSON oraz skutki uboczne w bazie danych. Poniżej żądanie POST tworzy zasób; sprawdzamy zarówno odpowiedź HTTP, jak i to, czy rekord rzeczywiście trafił do magazynu danych — dowodząc, że cały stos zadziałał poprawnie.
<?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'));
}
Wybiórcze zastępowanie zewnętrznych usług atrapami
Nawet testy funkcjonalne nie powinny wywoływać rzeczywistych operatorów płatności ani wysyłać prawdziwych wiadomości e-mail. Należy zastępować atrapami tylko najbardziej zewnętrzne usługi zewnętrzne — podmieniać je w kontenerze testowym na atrapy lub implementacje działające w pamięci — zachowując rzeczywisty własny kod, routing i bazę danych. Pozwala to zachować wierność testów end-to-end bez niestabilnych wywołań sieciowych.
<?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();
}
Osobne zestawy i różne czasy wykonywania
Podziel plik phpunit.xml na zestawy testów, aby szybki zestaw jednostkowy uruchamiał się przy każdym zapisie, a wolniejsze zestawy integracyjne i funkcjonalne były uruchamiane na żądanie lub w CI. Należy grupować testy według katalogów i wybierać je za pomocą --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>Determinizm ważniejszy niż pokrycie
Najszybszym sposobem na utratę zaufania do zestawu testów jest jego niestabilność. Testy integracyjne i funkcjonalne powinny być deterministyczne:
- Zamrażać czas (wstrzykiwać zegar) zamiast wywoływać
time(). - Ustalać ziarno losowości i unikać asercji zależnych od kolejności dla nieposortowanych zapytań.
- Resetować stan bazy danych po każdym teście (wycofywanie transakcji lub czyszczenie tabel).
- Nigdy nie uzależniać działania od kolejności wykonywania testów.
Dopasowanie testowej bazy danych do produkcji
SQLite w pamięci jest szybki, ale różnice między dialektami (uwzględnianie wielkości liter, funkcje JSON, wymuszanie kluczy obcych, typy) mogą ukrywać błędy ujawniające się dopiero na rzeczywistym silniku. W przypadku kodu korzystającego z zależnego od silnika SQL testy integracyjne należy uruchamiać na tym samym silniku co produkcja — często w jednorazowym kontenerze Docker w CI — zamiast używać wygodnego zamiennika.
<?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.
Szybkie sprawdzenie
Jak test funkcjonalny powinien traktować zewnętrznego dostawcę płatności?
Podsumowanie
Nauczyli się Państwo testować komponenty razem:
- Testy integracyjne korzystają z rzeczywistych baz danych i usług, aby wykrywać to, co ukrywają mocki; izoluje się je przez wycofywanie transakcji.
- Testy funkcjonalne sterują rzeczywistym jądrem za pomocą symulowanych żądań i sprawdzają odpowiedzi oraz skutki uboczne.
- Fabryki tworzą minimalny zestaw danych; atrapami zastępuje się tylko najbardziej zewnętrzne granice systemu.
- Zestawy testów należy dzielić według szybkości i dbać o determinizm wolniejszych testów.
Następny temat: pomiar jakości testów za pomocą testów mutacyjnych.
Często zadawane pytania
Czy lekcja „Testy integracyjne i funkcjonalne” jest bezpłatna?
Tak — pełny tekst „Testy integracyjne i funkcjonalne” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu PHP Academy, przejdź na CoddyKit PRO. Kurs PHP Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Testy integracyjne i funkcjonalne”?
Testuj, jak komponenty rzeczywiście współpracują Ćwiczysz PHP Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć PHP Academy?
Nie wymagamy żadnego doświadczenia. PHP Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Testy integracyjne i funkcjonalne”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji PHP Academy?
Tak. Każda lekcja PHP Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Przepływ pracy wytwarzania sterowanego testami
- Mockowanie i stubowanie za pomocą Mockery
- Testy integracyjne i funkcjonalne
- Testy mutacyjne za pomocą Infection