Интеграционное и функциональное тестирование
Проверяйте, как компоненты работают вместе в реальных условиях
«Интеграционное и функциональное тестирование» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Рабочий процесс разработки через тестирование
- Мокирование и заглушки с Mockery
- Интеграционное и функциональное тестирование
- Мутационное тестирование с Infection