0Pricing
PHP Academy · Урок

Интеграционное и функциональное тестирование

Проверяйте, как компоненты работают вместе в реальных условиях

«Интеграционное и функциональное тестирование» — бесплатный урок PHP Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.

За пределами модульных тестов

Модульные тесты доказывают, что класс работает изолированно, когда все зависимости заменены имитациями. Но имитации могут вводить в заблуждение: настоящая база данных, маршрутизатор фреймворка или слой HTTP ведут себя иначе. Интеграционные тесты проверяют два или более настоящих компонента, соединённых вместе; функциональные тесты проверяют приложение от начала до конца через его общедоступную точку входа — HTTP-запрос или команду консоли. В этом уроке вы разберёте назначение обоих видов и создадите их.

Пирамида тестов

Здоровый набор тестов имеет форму пирамиды:

  • Много модульных тестов — быстрые, изолированные, запускаются постоянно.
  • Меньше интеграционных тестов — настоящие БД, очереди и кэш, работают медленнее.
  • Немного функциональных и сквозных тестов — весь стек, самые медленные и хрупкие.

Если перевернуть эту структуру и получить «мороженое» из преимущественно сквозных тестов, набор станет медленным и нестабильным. По возможности переносите покрытие на нижние уровни.

Интеграция: репозиторий и настоящая БД

Интеграционный тест репозитория использует настоящее соединение с базой данных, а не имитацию, чтобы выявлять ошибки в SQL, схеме и сопоставлении данных. Используйте выделенную тестовую БД (или находящуюся в памяти SQLite, достаточно близкую к вашему диалекту) и настоящий 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);
    }
}

Изоляция: откат транзакции

Интеграционные тесты не должны передавать состояние друг другу. Самый быстрый подход — оборачивать каждый тест в транзакцию и откатывать её в tearDown(): БД возвращается к чистому состоянию без повторного заполнения. (Усечение — запасной вариант, если проверяемый код фиксирует транзакцию или использует 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
}

Тестовые данные и фабрики

Тестам нужны данные. Не используйте хрупкие огромные дампы SQL; предпочитайте небольшие фабрики, которые создают только строки, нужные тесту, с разумными значениями по умолчанию, переопределяемыми для каждого случая. Так назначение каждого теста остаётся явным и сохраняется устойчивым при расширении схемы.

<?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();
    }
}

Функциональные тесты: обращение к приложению как клиент

Функциональный тест запускает настоящее ядро и маршрутизатор приложения с имитированным запросом и проверяет ответ — код состояния, заголовки и тело. WebTestCase Symfony и вспомогательные средства HTTP-тестирования Laravel делают именно это, не запуская настоящий веб-сервер. Ниже показан вариант для 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());
    }
}

Проверка ответа

Функциональные тесты проверяют наблюдаемое поведение на границе: код состояния, структуру JSON и побочные эффекты в БД. Ниже запрос POST создаёт ресурс; мы проверяем и ответ HTTP, и то, что запись действительно попала в хранилище, — доказывая, что весь стек сработал согласованно.

<?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'));
}

Выборочная имитация внешних границ

Даже функциональные тесты не должны обращаться к реальным платёжным провайдерам или отправлять настоящие электронные письма. Заменяйте только внешние службы на самом внешнем уровне — подставляйте в контейнере тестирования имитации или реализации в памяти, сохраняя собственный код, маршрутизацию и БД настоящими. Это сохраняет достоверность сквозного сценария без нестабильных сетевых вызовов.

<?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();
}

Разные наборы, разная скорость

Разделите phpunit.xml на наборы тестов, чтобы быстрый набор модульных тестов запускался при каждом сохранении, а медленные интеграционные и функциональные наборы — по требованию или в среде непрерывной интеграции. Группируйте тесты по каталогам и выбирайте набор с помощью --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>

Детерминированность важнее покрытия

Самый быстрый способ утратить доверие к набору тестов — нестабильность. Сделайте интеграционные и функциональные тесты детерминированными:

  • Замораживайте время (внедряйте источник времени) вместо вызова time().
  • Фиксируйте начальное состояние генератора случайных чисел; избегайте зависящих от порядка проверок в неотсортированных запросах.
  • Сбрасывайте состояние БД после каждого теста (откат транзакции или усечение).
  • Никогда не зависите от порядка выполнения тестов.

Тестовая БД должна соответствовать рабочей

SQLite в памяти работает быстро, но различия диалектов (чувствительность к регистру, функции JSON, проверка внешних ключей, типы) могут скрывать ошибки, проявляющиеся только в вашем настоящем движке. Для кода, использующего SQL, специфичный для движка, запускайте интеграционные тесты на том же движке, что и рабочая среда, — обычно во временном контейнере Docker в среде непрерывной интеграции, — а не на удобной замене.

<?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.

Быстрая проверка

Как функциональный тест должен работать с платёжным провайдером третьей стороны?

Итоги

Вы научились проверять компоненты вместе:

  • Интеграционные тесты используют настоящие БД и службы, чтобы выявлять то, что скрывают имитации; изолируйте их откатом транзакции.
  • Функциональные тесты запускают настоящее ядро посредством имитированных запросов и проверяют ответы и побочные эффекты.
  • Фабрики создают минимальный набор данных; имитируйте только самые внешние границы внешних систем.
  • Разделяйте наборы по скорости и сохраняйте медленные тесты детерминированными.

Далее: измерение качества тестов с помощью мутационного тестирования.

Часто задаваемые вопросы

Урок «Интеграционное и функциональное тестирование» бесплатный?

Да — полный текст урока «Интеграционное и функциональное тестирование» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.

Чему я научусь в уроке «Интеграционное и функциональное тестирование»?

Проверяйте, как компоненты работают вместе в реальных условиях Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать PHP Academy?

Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Интеграционное и функциональное тестирование»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке PHP Academy?

Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Рабочий процесс разработки через тестирование
  2. Мокирование и заглушки с Mockery
  3. Интеграционное и функциональное тестирование
  4. Мутационное тестирование с Infection
← Назад к PHP Academy