統合テストと機能テスト
コンポーネントが実際に連携して動作するかをテストします。
「統合テストと機能テスト」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
ユニットテストの先へ
ユニットテストでは、すべてをモックした状態でクラスが単独で動作することを確認します。しかし、モックは誤解を招くことがあります。実際のデータベース、フレームワークのルーター、HTTP層は異なる動作をする可能性があります。統合テストでは、2つ以上の実際のコンポーネントを接続した状態で検証します。機能テストでは、公開されたエントリーポイント(HTTPリクエストやコンソールコマンド)を通じて、アプリケーションをエンドツーエンドで実行します。このレッスンでは、両方のテストの位置付けと書き方を扱います。
テストピラミッド
健全なテストスイートは、ピラミッド型になります。
- 多数のユニットテスト — 高速で、分離されており、常に実行します。
- 少数の統合テスト — 実際のDB・キュー・キャッシュを使用するため、より低速です。
- 少数の機能テスト/E2Eテスト — フルスタックで実行するため最も低速で、最も壊れやすくなります。
これを逆にした、E2Eテストが大半の「アイスクリームコーン」型では、低速で不安定なテストスイートになります。可能な限り、テストの対象を下位層に移してください。
統合テスト:実際のDBに対するリポジトリ
リポジトリの統合テストでは、モックではなく実際のデータベース接続を使用し、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()でロールバックする方法です。再シードせずに、DBをクリーンな状態へ戻せます。(テスト対象のコードがコミットを実行する場合や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();
}
}
機能テスト:クライアントのようにアプリケーションへアクセスする
機能テストでは、シミュレートしたリクエストで実際のアプリケーションカーネル/ルーターを動かし、レスポンス(ステータスコード、ヘッダー、本文)を検証します。SymfonyのWebTestCaseとLaravelのHTTPテストヘルパーは、実際のWebサーバーを起動せずにこれを実行します。以下では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の構造、DBへの副作用などです。以下では、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'));
}
境界部分を選択的にフェイクする
機能テストでも、実際の決済プロバイダーを呼び出したり、実際のメールを送信したりしてはいけません。最外部の外部サービスだけを置き換え、テストコンテナ内でフェイクまたはインメモリ実装に差し替えます。一方で、自分のコード、ルーティング、DBは実際のものを使い続けます。これにより、不安定なネットワーク呼び出しを避けながら、エンドツーエンドの忠実性を保てます。
<?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をテストスイートに分割し、高速なユニットテストスイートは保存のたびに実行し、低速な統合テスト/機能テストスイートは必要なとき、またはCIで実行します。ディレクトリ単位でグループ化し、--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()を呼び出すのではなく、クロックを注入して時間を固定します。- 乱数のシードを固定し、ソートされていないクエリに対する順序依存のアサーションを避けます。
- 各テストでDBの状態をリセットします(トランザクションのロールバック/トランケート)。
- テストの実行順序に決して依存しません。
テストDBを本番環境に合わせる
インメモリSQLiteは高速ですが、方言の違い(大文字と小文字の区別、JSON関数、外部キーの強制、型)によって、実際のエンジンでしか発生しないバグが隠れることがあります。エンジン固有のSQLを使用するコードでは、便利な代替環境ではなく、本番環境と同じエンジンに対して統合テストを実行してください。CIでは、通常、使い捨ての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.
簡単な確認
機能テストでは、サードパーティの決済プロバイダーをどのように扱うべきでしょうか。
まとめ
コンポーネントを組み合わせてテストする方法を学びました。
- 統合テストでは実際のDBやサービスを使用し、モックでは隠れてしまう問題を検出します。トランザクションのロールバックで分離してください。
- 機能テストでは、シミュレートしたリクエストを通じて実際のカーネルを動かし、レスポンスと副作用を検証します。
- ファクトリーで最小限のデータを構築し、最外部の外部境界だけをフェイクします。
- 速度でスイートを分割し、低速なテストも決定的に保ちます。
次は、ミューテーションテストによるテストの品質の測定です。
よくある質問
「統合テストと機能テスト」レッスンは無料ですか?
はい。「統合テストと機能テスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「統合テストと機能テスト」で何を学びますか?
コンポーネントが実際に連携して動作するかをテストします。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「統合テストと機能テスト」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。