0Pricing
PHP Academy · Lekcja

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

  1. Przepływ pracy wytwarzania sterowanego testami
  2. Mockowanie i stubowanie za pomocą Mockery
  3. Testy integracyjne i funkcjonalne
  4. Testy mutacyjne za pomocą Infection
← Powrót do PHP Academy