PHPのテストコードの書き方について

採用はこちら

PHPのテストコードを書くなら、まずは PHPUnit を中心に理解するのがおすすめです。

PHPUnitはPHPで広く使われているテストフレームワークで、通常のPHPプロジェクトはもちろん、Laravelなどのフレームワークでもテストの基礎として使われています。

テストコードは、単にバグを見つけるためだけのものではありません。

「この処理はこう動くべき」という仕様をコードとして残し、将来の修正やリファクタリングを安全に行うための仕組みです。

たとえば、価格計算・バリデーション・権限チェック・注文処理・ステータス変更など、アプリケーションにとって重要な処理は、テストコードを書いておくことで安心して改修できるようになります。

目次

PHPのテストコードとは

PHPのテストコードとは、プログラムが期待どおりに動いているかを自動で確認するためのコードです。

たとえば、税込価格を計算するクラスがあるとします。

<?php

declare(strict_types=1);

namespace App;

final class PriceCalculator
{
    public function taxIncluded(int $price, float $taxRate): int
    {
        return (int) floor($price * (1 + $taxRate));
    }
}

このメソッドに対して、

1000円に10%の税率をかけたら1100円になる

という仕様をテストコードで確認すると、次のようになります。

<?php

declare(strict_types=1);

namespace Tests;

use App\PriceCalculator;
use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
    public function testTaxIncludedReturnsPriceWithTax(): void
    {
        $calculator = new PriceCalculator();

        $result = $calculator->taxIncluded(1000, 0.1);

        $this->assertSame(1100, $result);
    }
}

このテストでは、taxIncluded(1000, 0.1) の結果が 1100 になることを確認しています。

結果が 1100 ならテスト成功、違う値ならテスト失敗です。

PHPでテストコードを書くメリット

バグを早期に発見できる

テストコードがあると、手動でブラウザを操作したり、画面を確認したりする前に、コマンドで処理の不具合を見つけられます。

特に、金額計算・日付計算・在庫判定・権限チェックのような処理は、条件分岐が増えるほどバグが入りやすくなります。

テストを書いておけば、修正後に既存の処理が壊れていないかをすぐに確認できます。

仕様をコードとして残せる

テストコードは、実行できる仕様書のような役割を持ちます。

たとえば、次のようなテストメソッド名があるとします。

public function testCannotCreateUserWithInvalidEmail(): void

この名前を見るだけで、「不正なメールアドレスではユーザーを作成できない」という仕様が分かります。

ドキュメントは更新されず古くなることがありますが、テストコードは実際に実行されるため、仕様と実装のズレに気づきやすくなります。

リファクタリングしやすくなる

テストがない状態でコードを整理すると、「本当に壊れていないか」を毎回手作業で確認しなければなりません。

一方、テストコードがあれば、処理の中身を変更したあとにテストを実行することで、外から見た振る舞いが変わっていないかを確認できます。

特に、古いPHPコードを改善する場合や、複雑なロジックを整理する場合には、テストコードがあるかどうかで作業の安心感が大きく変わります。

チーム開発で安心できる

チーム開発では、自分以外の人が書いたコードを修正する場面があります。

テストコードがあると、修正によって既存機能に影響が出ていないかを確認しやすくなります。

また、レビューする側も「どの仕様を守るためのコードなのか」をテストから読み取れるため、コードレビューの品質も上がります。

PHPテストの主な種類

単体テスト

単体テストは、1つの関数・クラス・メソッドなど、小さな単位で動作を確認するテストです。

たとえば、次のような処理が対象になります。

価格計算
バリデーション
文字列変換
ステータス判定
権限チェック
日付計算

PHPのテストコードを初めて書く場合は、まず単体テストから始めるのがおすすめです。

DBや外部APIに依存しないロジックであれば、比較的簡単にテストを書けます。

結合テスト

結合テストは、複数のクラスや処理を組み合わせた状態で動作を確認するテストです。

たとえば、ServiceクラスとRepositoryクラスを連携させて、DBからデータを取得できるかを確認するようなテストです。

単体テストよりも実際のアプリケーションに近い動作を確認できますが、その分、実行速度が遅くなったり、テストデータの準備が必要になったりします。

Featureテスト

Featureテストは、アプリケーションの機能単位で動作を確認するテストです。

Laravelでは、ログイン・会員登録・投稿作成・注文処理など、ユーザーが実際に使う機能をFeatureテストとして書くことが多いです。

たとえば、次のような内容を確認します。

ログインできるか
未ログインユーザーが管理画面にアクセスできないか
投稿フォームからデータを保存できるか
APIが正しいJSONを返すか

HTTPテスト

HTTPテストは、特定のURLにリクエストを送り、レスポンスが期待どおりかを確認するテストです。

Laravelでは、次のような形で簡単にHTTPテストを書けます。

public function testHomePageCanBeDisplayed(): void
{
    $response = $this->get('/');

    $response->assertStatus(200);
}

このテストでは、トップページにアクセスしたときにHTTPステータス 200 が返ることを確認しています。

DBテスト

DBテストは、データベースへの保存・取得・更新・削除が正しく行われるかを確認するテストです。

たとえば、フォームから送信された投稿データが posts テーブルに保存されているかを確認する場合などに使います。

Laravelでは、assertDatabaseHas() を使うと、DBに特定のデータが存在するかを確認できます。

$this->assertDatabaseHas('posts', [
    'title' => 'テストタイトル',
]);

E2Eテスト

E2Eテストは、ブラウザ操作を含めて、ユーザーの操作に近い形でアプリケーション全体を確認するテストです。

ログイン画面にアクセスし、メールアドレスとパスワードを入力し、ボタンを押してダッシュボードが表示されるかを確認するようなテストです。

ただし、E2Eテストは実行に時間がかかり、環境にも依存しやすいため、最初から大量に書くよりも、重要なフローに絞って書くのがおすすめです。

PHPUnitの導入方法

ComposerでPHPUnitをインストールする

PHPUnitはComposerでインストールできます。

composer require --dev phpunit/phpunit

インストール後、次のコマンドでバージョンを確認できます。

./vendor/bin/phpunit --version

テストを実行する基本コマンドは次の通りです。

./vendor/bin/phpunit

PHPUnitのバージョンに注意する

PHPUnitはバージョンによって必要なPHPバージョンが異なります。

たとえば、PHPUnit 13を使う場合はPHP 8.4以上が必要です。

一方で、既存のPHP 8.2やPHP 8.3のプロジェクトでは、PHPUnit 12、11、10などを使うケースもあります。

そのため、実務では単に最新版を入れるのではなく、プロジェクトのPHPバージョンに合わせてPHPUnitを選ぶ必要があります。

たとえば、PHPUnit 12を指定してインストールする場合は次のようにします。

composer require --dev phpunit/phpunit:^12

PHPUnit 13を使える環境であれば、次のように指定できます。

composer require --dev phpunit/phpunit:^13

既存プロジェクトでは、まず composer.json を確認し、現在のPHPバージョンやLaravelのバージョンと互換性があるPHPUnitを使うことが大切です。

PHPUnitを使うための基本構成

ディレクトリ構成の例

一般的なPHPプロジェクトでは、次のような構成にします。

project/
├── src/
│   └── PriceCalculator.php
├── tests/
│   └── PriceCalculatorTest.php
├── composer.json
└── phpunit.xml

src/ に本体コード、tests/ にテストコードを置きます。

Laravelの場合は、最初から次のようなディレクトリが用意されています。

tests/
├── Feature/
│   └── ExampleTest.php
├── Unit/
│   └── ExampleTest.php
└── TestCase.php

Laravelでは、単体テストは tests/Unit、機能テストは tests/Feature に置くのが一般的です。

composer.jsonのautoload設定

PHPUnitでテストを実行するには、Composerのautoload設定が必要です。

たとえば、次のように設定します。

{
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  },
  "require-dev": {
    "phpunit/phpunit": "^12"
  }
}

設定したら、次のコマンドを実行します。

composer dump-autoload

autoload設定がないと、テスト実行時に次のようなエラーが出ることがあります。

Class "App\PriceCalculator" not found

テストコードを書くときは、PHPUnitの書き方だけでなく、Composerのautoload設定もセットで理解しておく必要があります。

phpunit.xmlの基本設定

phpunit.xml は、PHPUnitの設定ファイルです。

最低限の例は次の通りです。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

bootstrap="vendor/autoload.php" を指定することで、Composerのautoloadが読み込まれます。

この設定により、src/ 配下のクラスや tests/ 配下のテストクラスをPHPUnitから扱えるようになります。

PHPUnitの基本的な書き方

TestCaseを継承する

PHPUnitのテストクラスは、基本的に PHPUnit\Framework\TestCase を継承します。

use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
}

テストクラス名は、テスト対象のクラス名に Test を付けるのが一般的です。

PriceCalculator
PriceCalculatorTest

このように対応させると、どのクラスのテストなのかが分かりやすくなります。

testで始まるメソッドを書く

PHPUnitでは、test で始まるpublicメソッドがテストメソッドとして実行されます。

public function testTaxIncludedReturnsPriceWithTax(): void
{
}

また、PHPUnitのバージョンによっては #[Test] 属性を使ってテストメソッドとして扱うこともできます。

use PHPUnit\Framework\Attributes\Test;

#[Test]
public function taxIncludedReturnsPriceWithTax(): void
{
}

ただし、初心者のうちは test で始まるメソッド名にしておくと分かりやすいです。

assertで結果を確認する

テストでは、処理の結果が期待どおりかをアサーションで確認します。

$this->assertSame(1100, $result);

assertSame() は、期待値と実際の値が型も含めて同じかを確認します。

PHPでは、数値の 100 と文字列の '100' が混同されるとバグにつながることがあります。

そのため、基本的には assertEquals() よりも assertSame() を優先すると安全です。

Arrange / Act / Assertを意識する

テストコードの基本構造

テストコードは、次の3つの流れで書くと分かりやすくなります。

Arrange:準備
Act:実行
Assert:検証

たとえば、税込価格を計算するテストは次のように書けます。

public function testTaxIncludedReturnsPriceWithTax(): void
{
    // Arrange
    $calculator = new PriceCalculator();
    $price = 1000;
    $taxRate = 0.1;

    // Act
    $result = $calculator->taxIncluded($price, $taxRate);

    // Assert
    $this->assertSame(1100, $result);
}

コメントは必須ではない

ArrangeActAssert のコメントは、初心者のうちは書いてもよいです。

ただし、慣れてきたら毎回コメントを書く必要はありません。

コード自体が短く、流れが明確であれば、コメントなしでも十分読みやすくなります。

public function testTaxIncludedReturnsPriceWithTax(): void
{
    $calculator = new PriceCalculator();

    $result = $calculator->taxIncluded(1000, 0.1);

    $this->assertSame(1100, $result);
}

重要なのは、テストの中で「準備」「実行」「検証」が混ざりすぎないようにすることです。

よく使うPHPUnitのアサーション

assertSame

assertSame() は、期待値と実際の値が型も含めて同じかを確認します。

$this->assertSame(100, $result);

PHPのテストでは、基本的に assertSame() を優先して使うのがおすすめです。

assertEquals

assertEquals() は、値が等しいかを確認します。

$this->assertEquals(100, $result);

ただし、型の厳密さは assertSame() より弱いため、意図せずテストが通ってしまうことがあります。

$this->assertEquals(100, '100'); // 通る可能性がある
$this->assertSame(100, '100');  // 失敗する

型まで厳密に確認したい場合は、assertSame() を使いましょう。

assertTrue

assertTrue() は、結果が true であることを確認します。

$this->assertTrue($user->isActive());

assertFalse

assertFalse() は、結果が false であることを確認します。

$this->assertFalse($user->isDeleted());

assertNull

assertNull() は、値が null であることを確認します。

$this->assertNull($user->deletedAt());

assertCount

assertCount() は、配列やCountableなオブジェクトの件数を確認します。

$this->assertCount(3, $items);

assertInstanceOf

assertInstanceOf() は、指定したクラスのインスタンスであることを確認します。

$this->assertInstanceOf(User::class, $user);

assertContains

assertContains() は、配列などに特定の値が含まれているかを確認します。

$this->assertContains('admin', $roles);

assertArrayHasKey

assertArrayHasKey() は、配列に特定のキーが存在するかを確認します。

$this->assertArrayHasKey('email', $data);

例外のテストを書く方法

expectExceptionを使う

不正な入力が来たときに例外を投げる処理もテストできます。

たとえば、メールアドレスを表すクラスがあるとします。

<?php

declare(strict_types=1);

namespace App;

use InvalidArgumentException;

final class Email
{
    public function __construct(
        private string $value
    ) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email address.');
        }
    }

    public function value(): string
    {
        return $this->value;
    }
}

有効なメールアドレスで作成できることを確認するテストは次の通りです。

public function testCanCreateEmailWithValidAddress(): void
{
    $email = new Email('test@example.com');

    $this->assertSame('test@example.com', $email->value());
}

不正なメールアドレスで例外が発生することを確認するテストは次のようになります。

public function testThrowsExceptionWithInvalidAddress(): void
{
    $this->expectException(InvalidArgumentException::class);

    new Email('invalid-email');
}

expectExceptionは例外発生前に書く

重要なのは、expectException() を例外が発生する処理の前に書くことです。

$this->expectException(InvalidArgumentException::class);

new Email('invalid-email');

順番が逆になると、PHPUnitが例外を期待している状態になる前に例外が発生してしまい、意図したテストになりません。

データプロバイダで複数パターンをテストする

DataProviderを使う

同じメソッドを複数の入力値でテストしたい場合は、データプロバイダが便利です。

税込価格計算を複数パターンで確認する例です。

<?php

declare(strict_types=1);

namespace Tests;

use App\PriceCalculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
    #[DataProvider('taxIncludedProvider')]
    public function testTaxIncluded(
        int $price,
        float $taxRate,
        int $expected
    ): void {
        $calculator = new PriceCalculator();

        $result = $calculator->taxIncluded($price, $taxRate);

        $this->assertSame($expected, $result);
    }

    public static function taxIncludedProvider(): array
    {
        return [
            '1000円の10%税率' => [1000, 0.1, 1100],
            '2000円の10%税率' => [2000, 0.1, 2200],
            '999円の10%税率' => [999, 0.1, 1098],
        ];
    }
}

データプロバイダを使うと、同じようなテストメソッドを何度も書かずに済みます。

データセットには名前を付ける

データプロバイダでは、各データセットに名前を付けるのがおすすめです。

return [
    '1000円の10%税率' => [1000, 0.1, 1100],
    '2000円の10%税率' => [2000, 0.1, 2200],
];

名前を付けておくと、テストが失敗したときに、どのケースで失敗したのかが分かりやすくなります。

TestWithを使う方法もある

データが少ない場合は、TestWith 属性を使う方法もあります。

use PHPUnit\Framework\Attributes\TestWith;

#[TestWith([1000, 0.1, 1100])]
#[TestWith([2000, 0.1, 2200])]
#[TestWith([999, 0.1, 1098])]
public function testTaxIncluded(
    int $price,
    float $taxRate,
    int $expected
): void {
    $calculator = new PriceCalculator();

    $this->assertSame(
        $expected,
        $calculator->taxIncluded($price, $taxRate)
    );
}

少ないデータなら TestWith でも問題ありません。

ただし、パターンが増える場合やデータセットに名前を付けたい場合は、DataProvider のほうが読みやすくなります。

setUpを使って共通の準備をする

setUpの基本

複数のテストで同じ準備が必要な場合は、setUp() を使えます。

<?php

declare(strict_types=1);

namespace Tests;

use App\PriceCalculator;
use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new PriceCalculator();
    }

    public function testTaxIncludedWithTenPercent(): void
    {
        $this->assertSame(
            1100,
            $this->calculator->taxIncluded(1000, 0.1)
        );
    }

    public function testTaxIncludedWithEightPercent(): void
    {
        $this->assertSame(
            1080,
            $this->calculator->taxIncluded(1000, 0.08)
        );
    }
}

setUp() は、各テストメソッドの実行前に呼ばれます。

setUpに書きすぎない

setUp() は便利ですが、何でも入れればよいわけではありません。

大量の準備処理を setUp() に書くと、各テストの前提条件が分かりにくくなります。

おすすめの使い方は次の通りです。

全テストで本当に必要なものだけsetUpに書く
テストごとに違うデータはテストメソッド内で準備する
複雑な生成処理はprivateメソッドに切り出す

Laravelではparent::setUpを呼ぶ

Laravelのテストで setUp() を使う場合は、基本的に parent::setUp() を呼びます。

protected function setUp(): void
{
    parent::setUp();

    // 追加の準備処理
}

parent::setUp() を呼ばないと、Laravel側のテスト環境の初期化が正しく行われないことがあります。

privateメソッドでテストデータを作る

テスト対象の生成をメソッド化する

複雑なオブジェクトを毎回作る場合は、privateメソッドに切り出すと読みやすくなります。

final class PriceCalculatorTest extends TestCase
{
    public function testTaxIncludedWithTenPercent(): void
    {
        $calculator = $this->createCalculator();

        $this->assertSame(1100, $calculator->taxIncluded(1000, 0.1));
    }

    private function createCalculator(): PriceCalculator
    {
        return new PriceCalculator();
    }
}

単純な例では効果が小さいですが、依存オブジェクトが増えると便利です。

複雑なServiceを作る例

たとえば、注文処理のServiceを作る場合です。

private function createOrderService(): OrderService
{
    $repository = new InMemoryOrderRepository();
    $mailer = new FakeMailer();

    return new OrderService($repository, $mailer);
}

このようにしておくと、各テストメソッドでは本質的な処理に集中できます。

モック・スタブ・Fakeを使う

テストダブルとは

テスト対象が外部API、メール送信、DB、ファイル保存などに依存している場合、実物を使うとテストが不安定になります。

そこで、実際の依存オブジェクトの代わりに使うものを テストダブル と呼びます。

代表的なものには、次のような種類があります。

スタブ:戻り値を固定する
モック:メソッドが呼ばれたか確認する
Fake:テスト用の簡易実装を使う

スタブの使い方

スタブは、依存オブジェクトの戻り値を固定したいときに使います。

interface UserRepository
{
    public function findById(int $id): ?User;
}
final class UserService
{
    public function __construct(
        private UserRepository $repository
    ) {
    }

    public function getUserName(int $id): string
    {
        $user = $this->repository->findById($id);

        if ($user === null) {
            return 'Guest';
        }

        return $user->name();
    }
}

テストでは、Repositoryの戻り値を固定します。

public function testGetUserNameReturnsUserName(): void
{
    $user = new User('Taro');

    $repository = $this->createStub(UserRepository::class);
    $repository
        ->method('findById')
        ->willReturn($user);

    $service = new UserService($repository);

    $this->assertSame('Taro', $service->getUserName(1));
}

ユーザーが見つからないケースもテストできます。

public function testGetUserNameReturnsGuestWhenUserNotFound(): void
{
    $repository = $this->createStub(UserRepository::class);
    $repository
        ->method('findById')
        ->willReturn(null);

    $service = new UserService($repository);

    $this->assertSame('Guest', $service->getUserName(999));
}

モックの使い方

モックは、特定のメソッドが期待どおり呼ばれたかを確認するときに使います。

たとえば、会員登録時にウェルカムメールを送信するServiceがあるとします。

interface Mailer
{
    public function sendWelcomeMail(string $email): void;
}

final class UserRegistrationService
{
    public function __construct(
        private Mailer $mailer
    ) {
    }

    public function register(string $email): void
    {
        $this->mailer->sendWelcomeMail($email);
    }
}

テストでは、メール送信メソッドが1回呼ばれることを確認します。

public function testRegisterSendsWelcomeMail(): void
{
    $mailer = $this->createMock(Mailer::class);

    $mailer
        ->expects($this->once())
        ->method('sendWelcomeMail')
        ->with('test@example.com');

    $service = new UserRegistrationService($mailer);

    $service->register('test@example.com');
}

モックの使いすぎに注意する

モックは便利ですが、使いすぎると実装の詳細に依存したテストになりやすいです。

たとえば、内部メソッドの呼び出し回数や呼び出し順序を細かく確認しすぎると、リファクタリングしただけでテストが壊れることがあります。

基本的には、次のように使い分けるとよいです。

戻り値を固定したいだけならスタブ
副作用を記録したいならFake
呼び出し自体が仕様ならモック

Fakeを使う方法

メール送信のような処理では、モックではなくFakeを使うと読みやすくなることがあります。

final class FakeMailer implements Mailer
{
    public array $sentEmails = [];

    public function sendWelcomeMail(string $email): void
    {
        $this->sentEmails[] = $email;
    }
}

テストは次のように書けます。

public function testRegisterSendsWelcomeMail(): void
{
    $mailer = new FakeMailer();

    $service = new UserRegistrationService($mailer);

    $service->register('test@example.com');

    $this->assertSame(['test@example.com'], $mailer->sentEmails);
}

この方法では、実際にメールは送信せず、送信されたはずのメールアドレスだけを記録して確認できます。

テストしやすいPHPコードの書き方

依存を直接newしない

テストしにくいコードの代表例は、メソッドの中で依存オブジェクトを直接 new しているコードです。

final class UserRegistrationService
{
    public function register(string $email): void
    {
        $mailer = new SmtpMailer();

        $mailer->sendWelcomeMail($email);
    }
}

この書き方だと、テスト時に SmtpMailer を差し替えにくくなります。

改善するなら、コンストラクタで依存オブジェクトを受け取ります。

final class UserRegistrationService
{
    public function __construct(
        private Mailer $mailer
    ) {
    }

    public function register(string $email): void
    {
        $this->mailer->sendWelcomeMail($email);
    }
}

この形にしておけば、テスト時にモック・スタブ・Fakeを渡せます。

現在時刻を直接呼ばない

現在時刻に依存するコードも、テストしにくくなりがちです。

final class CampaignService
{
    public function isActive(): bool
    {
        return new DateTimeImmutable() < new DateTimeImmutable('2026-12-31');
    }
}

このコードは、実行日によって結果が変わります。

そのため、テストも日付によって成功したり失敗したりする可能性があります。

改善するなら、現在時刻を取得する役割を外から渡します。

interface Clock
{
    public function now(): DateTimeImmutable;
}
final class CampaignService
{
    public function __construct(
        private Clock $clock
    ) {
    }

    public function isActive(): bool
    {
        return $this->clock->now() < new DateTimeImmutable('2026-12-31');
    }
}

テストでは、固定時刻を返すClockを使います。

final class FixedClock implements Clock
{
    public function __construct(
        private DateTimeImmutable $now
    ) {
    }

    public function now(): DateTimeImmutable
    {
        return $this->now;
    }
}
public function testCampaignIsActiveBeforeEndDate(): void
{
    $clock = new FixedClock(new DateTimeImmutable('2026-01-01'));
    $service = new CampaignService($clock);

    $this->assertTrue($service->isActive());
}

外部APIを直接呼ばない

外部APIに直接依存するコードもテストが不安定になります。

final class PaymentService
{
    public function charge(int $amount): void
    {
        file_get_contents('https://payment.example.com/charge');
    }
}

このようなコードでは、ネットワーク状態や外部サービスの状況によってテスト結果が変わってしまいます。

外部APIを扱う場合は、インターフェースを用意し、テスト時にはFakeやスタブに差し替える設計にすると安全です。

Laravelでのテストコードの書き方

Laravelのテスト構成

Laravelでは、標準で tests ディレクトリが用意されています。

tests/
├── Feature/
├── Unit/
└── TestCase.php

基本的には、ロジック単体のテストは Unit、HTTPリクエストを含む機能テストは Feature に置きます。

Laravelのテスト実行コマンド

Laravelでは、次のコマンドでテストを実行できます。

php artisan test

また、PHPUnitを直接実行することもできます。

./vendor/bin/phpunit

Pestを使っているプロジェクトでは、次のコマンドも使われます。

./vendor/bin/pest

Laravelでは、通常 php artisan test を使うと分かりやすいです。

HTTPテストの例

トップページが表示できるかを確認するテストです。

<?php

namespace Tests\Feature;

use Tests\TestCase;

final class HomeTest extends TestCase
{
    public function testHomePageCanBeDisplayed(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

このテストでは、/ にGETリクエストを送り、ステータスコードが 200 であることを確認しています。

認証が必要なページのテスト

ログイン済みユーザーがダッシュボードにアクセスできるかを確認する例です。

public function testAuthenticatedUserCanAccessDashboard(): void
{
    $user = User::factory()->create();

    $response = $this
        ->actingAs($user)
        ->get('/dashboard');

    $response->assertStatus(200);
}

actingAs() を使うことで、指定したユーザーとしてログインした状態を作れます。

JSON APIのテスト

APIのレスポンス構造を確認する例です。

public function testUserListApiReturnsUsers(): void
{
    $response = $this->getJson('/api/users');

    $response
        ->assertStatus(200)
        ->assertJsonStructure([
            'data' => [
                '*' => [
                    'id',
                    'name',
                    'email',
                ],
            ],
        ]);
}

JSON APIでは、ステータスコードだけでなく、レスポンスの構造もテストしておくと安心です。

DB保存を確認するテスト

LaravelでDBを使うテストを書く場合は、RefreshDatabase を使うのが一般的です。

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

final class PostTest extends TestCase
{
    use RefreshDatabase;

    public function testCanCreatePost(): void
    {
        $user = User::factory()->create();

        $response = $this
            ->actingAs($user)
            ->post('/posts', [
                'title' => 'テストタイトル',
                'body' => 'テスト本文',
            ]);

        $response->assertRedirect('/posts');

        $this->assertDatabaseHas('posts', [
            'title' => 'テストタイトル',
            'body' => 'テスト本文',
        ]);
    }
}

RefreshDatabase を使うことで、テストごとにDBの状態をリセットしやすくなります。

これにより、前のテストのデータが次のテストに影響することを防げます。

PestでPHPのテストを書く方法

Pestとは

Pestは、PHPUnitの上に作られたテストフレームワークです。

PHPUnitよりも簡潔な記法でテストを書けるため、Laravelプロジェクトでもよく使われています。

PHPUnitのテストはクラスベースで書きますが、Pestでは次のように関数ベースで書けます。

it('calculates tax included price', function () {
    $calculator = new PriceCalculator();

    expect($calculator->taxIncluded(1000, 0.1))->toBe(1100);
});

LaravelでPestを使う例

LaravelのHTTPテストをPestで書くと、次のようになります。

it('displays home page', function () {
    $this->get('/')
        ->assertStatus(200);
});

PHPUnitよりも短く書けるため、読みやすさを重視するプロジェクトではPestが好まれることがあります。

PHPUnitとPestの違い

PHPUnitとPestの違いは、主に書き方です。

PHPUnit:クラスベースで書く
Pest:関数ベースで簡潔に書く

PHPUnitは歴史が長く、既存のPHPプロジェクトでも広く使われています。

Pestは記述量が少なく、Laravelとの相性も良いため、新しいプロジェクトで採用されることがあります。

初心者は、まずPHPUnitの基本を理解してからPestを使うとスムーズです。

Pestを使う場合でも、内部的にはPHPUnitの考え方が役立ちます。

テスト名の付け方

何を確認しているか分かる名前にする

テスト名は、テストコードの読みやすさに大きく影響します。

悪い例です。

public function testUser(): void

これでは、ユーザーの何をテストしているのか分かりません。

良い例です。

public function testCanCreateUserWithValidEmail(): void
public function testCannotCreateUserWithInvalidEmail(): void
public function testGuestUserCannotAccessDashboard(): void

テスト名を見るだけで、何を確認しているのか分かるようにしましょう。

実務では英語名が無難

PHPでは日本語のメソッド名が使える場合もあります。

public function test有効なメールアドレスでユーザーを作成できる(): void

ただし、実務では英語のテスト名のほうが無難です。

理由は、チーム開発・CI環境・エディタ・静的解析ツールとの相性を考えると、英語名のほうがトラブルが少ないためです。

何をテストすべきか

金額計算

ECサイトや請求システムでは、金額計算のバグが大きな問題につながります。

public function testDiscountPriceIsCalculatedCorrectly(): void

税込価格、割引、送料、手数料、ポイント利用などは、優先的にテストを書くべき処理です。

バリデーション

入力値の検証も、テスト対象として重要です。

public function testInvalidEmailIsRejected(): void

メールアドレス、パスワード、電話番号、郵便番号、必須項目などは、正常系と異常系の両方をテストしておくと安心です。

権限チェック

権限チェックは、セキュリティに関わる重要な処理です。

public function testGuestCannotAccessAdminPage(): void

管理者だけがアクセスできる画面や、自分の投稿だけ編集できる機能などは、必ずテストしておきたい部分です。

状態遷移

注文・予約・申請・承認など、状態が変化する処理もテストに向いています。

public function testOrderCanBeCancelledWhenPending(): void

たとえば、注文ステータスが pending のときだけキャンセルできる、発送後はキャンセルできない、といった仕様はテストで明確にしておくべきです。

例外・エラーケース

正常系だけでなく、異常系もテストしましょう。

public function testThrowsExceptionWhenStockIsInsufficient(): void

在庫不足、権限なし、不正な入力、存在しないデータへのアクセスなどは、テストしておくとバグを防ぎやすくなります。

テスト優先度が低いもの

単純なgetterやsetter

次のような単純なgetterやsetterは、優先度が高くありません。

public function getName(): string
{
    return $this->name;
}

ロジックがほとんどないため、テストを書いても得られる効果が小さいことがあります。

フレームワーク自体の機能

LaravelのルーティングやEloquentの基本機能など、フレームワークが保証している機能を細かくテストしすぎる必要はありません。

もちろん、自分のアプリケーションとして重要な振る舞いはテストすべきですが、フレームワークそのものの動作確認になっているテストは避けたほうがよいです。

変更頻度が高いHTMLの細部

HTMLのclass名や文言など、頻繁に変わる部分を細かくテストしすぎると、少しデザインを変更しただけでテストが壊れます。

画面テストでは、細かい見た目よりも、重要な要素が表示されているか、フォーム送信ができるか、権限チェックが正しく動くかを優先しましょう。

実務で使いやすいテストコードの型

戻り値を確認するテスト

public function testSomethingReturnsExpectedValue(): void
{
    $service = new SomeService();

    $result = $service->execute('input');

    $this->assertSame('expected', $result);
}

もっとも基本的なテストの形です。

状態が変わることを確認するテスト

public function testUserCanBeActivated(): void
{
    $user = new User('test@example.com');

    $user->activate();

    $this->assertTrue($user->isActive());
}

オブジェクトの状態が変化する処理では、実行後の状態を確認します。

例外が発生することを確認するテスト

public function testThrowsExceptionWhenInputIsInvalid(): void
{
    $this->expectException(InvalidArgumentException::class);

    $service = new SomeService();
    $service->execute('');
}

不正な入力や許可されていない操作では、例外が発生することをテストします。

メソッドが呼ばれることを確認するテスト

public function testMailerIsCalled(): void
{
    $mailer = $this->createMock(Mailer::class);

    $mailer
        ->expects($this->once())
        ->method('send')
        ->with('test@example.com');

    $service = new NotificationService($mailer);

    $service->notify('test@example.com');
}

外部通知やメール送信など、呼び出し自体が重要な仕様である場合に使います。

DBに保存されたことを確認するLaravelテスト

public function testPostIsStored(): void
{
    $user = User::factory()->create();

    $this
        ->actingAs($user)
        ->post('/posts', [
            'title' => 'タイトル',
            'body' => '本文',
        ]);

    $this->assertDatabaseHas('posts', [
        'title' => 'タイトル',
    ]);
}

Laravelでは、assertDatabaseHas() を使ってDBの状態を確認できます。

テストコードを書くときのコツ

1つのテストでは1つの振る舞いを確認する

悪い例です。

public function testUser(): void
{
    // 作成できる
    // 更新できる
    // 削除できる
    // ログインできる
}

このように1つのテストに多くの確認を詰め込むと、失敗したときに原因が分かりにくくなります。

良い例です。

public function testUserCanBeCreated(): void
public function testUserCanBeUpdated(): void
public function testUserCanBeDeleted(): void

テストは、できるだけ小さく分けましょう。

実装ではなく振る舞いをテストする

テストでは、内部実装よりも外から見える振る舞いを確認することが大切です。

たとえば、privateメソッドが呼ばれたかどうかを直接テストするよりも、publicメソッドを実行した結果が期待どおりかを確認しましょう。

内部実装に依存したテストは、リファクタリングで壊れやすくなります。

テストデータは最小限にする

テストに関係のないデータを大量に用意すると、何を確認したいテストなのかが分かりにくくなります。

悪い例です。

$user = new User(
    name: 'Taro',
    email: 'taro@example.com',
    address: 'Tokyo',
    phone: '090...',
    company: 'Example Inc.',
    birthday: '1990-01-01',
);

良い例です。

$user = new User(
    name: 'Taro',
    email: 'taro@example.com',
);

テストの意図に関係するデータだけを用意しましょう。

マジックナンバーを避ける

次のようなコードは、なぜ 1080 になるのか分かりにくい場合があります。

$this->assertSame(1080, $result);

必要に応じて、変数に分けると読みやすくなります。

$price = 1000;
$taxRate = 0.08;
$expected = 1080;

$this->assertSame($expected, $calculator->taxIncluded($price, $taxRate));

ただし、単純なテストでは変数を増やしすぎると逆に読みにくくなることもあります。

読みやすさを基準に判断しましょう。

テストしにくいコードの典型例

グローバル関数に依存している

final class ReportService
{
    public function generate(): string
    {
        $date = date('Y-m-d');

        return "Report: {$date}";
    }
}

このコードは、実行日によって結果が変わるため、テストしにくくなります。

外部APIを直接呼んでいる

final class PaymentService
{
    public function charge(int $amount): void
    {
        file_get_contents('https://payment.example.com/charge');
    }
}

外部APIを直接呼ぶと、ネットワークや外部サービスの状態に依存してしまいます。

Controllerに処理を詰め込みすぎている

final class OrderController
{
    public function store(Request $request)
    {
        // バリデーション
        // 在庫チェック
        // 金額計算
        // DB保存
        // メール送信
        // レスポンス生成
    }
}

Controllerに処理を詰め込みすぎると、テストが難しくなります。

改善するなら、ビジネスロジックをServiceやUseCaseに切り出します。

Controller
↓
UseCase / Service
↓
Domain Object / Repository

テストしやすい設計の例

注文処理をServiceに切り出す

たとえば、注文処理をControllerに直接書くのではなく、Serviceに切り出します。

final class OrderService
{
    public function order(User $user, Cart $cart): Order
    {
        if ($cart->isEmpty()) {
            throw new EmptyCartException();
        }

        $totalPrice = $cart->totalPrice();

        return new Order($user, $cart->items(), $totalPrice);
    }
}

このようにすると、注文ロジックだけをテストしやすくなります。

空のカートでは注文できないことをテストする

public function testCannotOrderWithEmptyCart(): void
{
    $this->expectException(EmptyCartException::class);

    $user = new User('test@example.com');
    $cart = new Cart([]);

    $service = new OrderService();

    $service->order($user, $cart);
}

カートから注文を作成できることをテストする

public function testCanCreateOrderFromCart(): void
{
    $user = new User('test@example.com');
    $cart = new Cart([
        new CartItem('商品A', 1000, 2),
        new CartItem('商品B', 500, 1),
    ]);

    $service = new OrderService();

    $order = $service->order($user, $cart);

    $this->assertSame(2500, $order->totalPrice());
}

このようにServiceに切り出しておくと、HTTPやDBに依存せず、ビジネスロジックを直接テストできます。

カバレッジの考え方

カバレッジとは

カバレッジとは、テストによってどれくらいのコードが実行されたかを示す指標です。

たとえば、カバレッジ80%なら、全体のコードのうち80%がテスト実行時に通ったという意味です。

カバレッジだけを目標にしない

カバレッジが高いからといって、必ずしも品質が高いとは限りません。

たとえば、次のようなテストは意味がありません。

$this->assertTrue(true);

このようなテストを書いても、仕様を守れているかどうかは確認できません。

大切なのは、カバレッジの数字そのものよりも、重要な仕様がテストされているかです。

優先すべきなのは、次のような部分です。

重要なビジネスロジック
バグが出やすい条件分岐
金額・権限・状態遷移
過去にバグが出た箇所

TDDの基本

TDDとは

TDDは、Test Driven Developmentの略で、テスト駆動開発と呼ばれます。

基本の流れは次の通りです。

1. 失敗するテストを書く
2. テストが通る最小限の実装を書く
3. リファクタリングする

TDDの流れ

まず、テストを書きます。

public function testTaxIncludedReturnsPriceWithTax(): void
{
    $calculator = new PriceCalculator();

    $this->assertSame(1100, $calculator->taxIncluded(1000, 0.1));
}

この時点では、PriceCalculator が存在しなければテストは失敗します。

次に、テストが通る最小限の実装を書きます。

final class PriceCalculator
{
    public function taxIncluded(int $price, float $taxRate): int
    {
        return 1100;
    }
}

これでテストは通るかもしれませんが、実装としては不十分です。

そこで、より一般的な実装に修正します。

final class PriceCalculator
{
    public function taxIncluded(int $price, float $taxRate): int
    {
        return (int) floor($price * (1 + $taxRate));
    }
}

その後、複数パターンのテストを追加して、仕様を固めていきます。

テストコードのアンチパターン

実装に依存しすぎる

内部メソッドの呼び出し順序や回数を細かく確認しすぎると、リファクタリングで壊れやすいテストになります。

テストでは、できるだけ外から見た振る舞いを確認しましょう。

1つのテストが長すぎる

1つのテストが長すぎると、失敗したときに原因を追いにくくなります。

複数の仕様を確認している場合は、テストを分けることを検討しましょう。

外部APIに直接アクセスする

外部APIに直接アクセスするテストは、ネットワークや外部サービスの状態に左右されます。

外部APIは、スタブやFakeに差し替えてテストするのが基本です。

テスト同士が依存している

「Aのテストが先に実行されていること」を前提にしたテストは避けましょう。

テストは、どの順番で実行されても同じ結果になるべきです。

現在時刻に依存している

現在時刻に依存するテストは、日付が変わると失敗する可能性があります。

現在時刻はClockクラスなどで外から渡せるようにすると、テストしやすくなります。

ランダム値に依存している

ランダム値に依存すると、テストが再現しにくくなります。

$name = uniqid();

このような値を期待値に絡める場合は注意が必要です。

PHPのテストコードを始める手順

純粋な関数・クラスから始める

最初は、DBやHTTPに依存しない小さなロジックから始めましょう。

たとえば、次のような処理です。

価格計算
バリデーション
文字列整形
日付計算
権限判定
ステータス判定

これらはテストを書きやすく、効果も分かりやすいです。

正常系と異常系を書く

正常に動くケースだけでなく、エラーになるケースもテストします。

public function testThrowsExceptionWhenEmailIsInvalid(): void

異常系のテストがあると、不正な入力や想定外の操作に対する安全性を確認できます。

複数パターンはデータプロバイダで整理する

入力値の組み合わせが増えてきたら、データプロバイダを使いましょう。

#[DataProvider('taxIncludedProvider')]

同じようなテストを何度も書かずに済み、テストコードを整理しやすくなります。

外部依存は差し替えられるようにする

メール送信、外部API、現在時刻、ファイル保存などは、テスト時に差し替えられる設計にしておきましょう。

インターフェースを使って依存を外から渡す形にすると、テストがかなり書きやすくなります。

LaravelならFeatureテストも書く

Laravelプロジェクトでは、単体テストだけでなくFeatureテストも重要です。

ログイン、投稿作成、注文処理、APIレスポンスなど、ユーザーの操作に近い単位でテストを書くことで、アプリケーション全体の動作を確認できます。

PHPテストコードの練習問題

消費税計算のテスト

まずは、消費税計算のような単純なロジックから練習するとよいです。

final class TaxCalculator
{
    public function calculate(int $price, float $rate): int
    {
        return (int) floor($price * (1 + $rate));
    }
}

テストすべきケースは次の通りです。

1000円・10% → 1100
1000円・8% → 1080
999円・10% → 1098
0円 → 0

メールアドレスのバリデーションテスト

final class Email
{
    public function __construct(private string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException();
        }
    }
}

テストすべきケースは次の通りです。

test@example.com はOK
invalid-email は例外
空文字は例外

パスワード強度チェックのテスト

final class PasswordPolicy
{
    public function isValid(string $password): bool
    {
        return strlen($password) >= 8;
    }
}

テストすべきケースは次の通りです。

8文字以上ならtrue
7文字以下ならfalse
空文字ならfalse

送料無料判定のテスト

final class ShippingFeeService
{
    public function isFreeShipping(int $totalPrice): bool
    {
        return $totalPrice >= 5000;
    }
}

テストすべきケースは次の通りです。

4999円 → false
5000円 → true
5001円 → true

PHPテストでまず覚えるべき最小セット

基本のuse文

use PHPUnit\Framework\TestCase;

テストクラス

final class XxxTest extends TestCase
{
}

テストメソッド

public function testSomething(): void
{
}

期待値と実際の値を比較する

$this->assertSame($expected, $actual);

trueを確認する

$this->assertTrue($actual);

falseを確認する

$this->assertFalse($actual);

例外を確認する

$this->expectException(SomeException::class);

複数パターンを確認する

#[DataProvider('providerName')]

共通の準備をする

protected function setUp(): void
{
}

PHPのテストコードを書くときの注意点

金額計算では丸め処理に注意する

税込価格の例では、次のように floor() を使いました。

return (int) floor($price * (1 + $taxRate));

ただし、実務の金額計算では注意が必要です。

小数点以下を切り捨てるのか、四捨五入するのか、切り上げるのかは、業務仕様によって変わります。

floor(); // 切り捨て
round(); // 四捨五入
ceil();  // 切り上げ

また、金額計算でfloatを使うと丸め誤差が問題になることがあります。

会計・決済・請求など厳密さが必要な領域では、整数の円単位で扱う、またはBCMathなどの利用を検討する必要があります。

バージョン差に注意する

PHPUnit、Pest、Laravelは、バージョンによって書き方や必要なPHPバージョンが異なります。

特に、次の点は確認しておきましょう。

PHPのバージョン
PHPUnitのバージョン
Laravelのバージョン
Pestのバージョン
composer.jsonの依存関係
phpunit.xmlの設定

サンプルコードをそのまま使う場合でも、自分のプロジェクトの環境に合わせて調整することが大切です。

まとめ

PHPのテストコードを書くなら、まずはPHPUnitの基本を押さえるのがおすすめです。

基本の形は次の通りです。

public function testSomething(): void
{
    // 準備
    $service = new SomeService();

    // 実行
    $result = $service->execute();

    // 検証
    $this->assertSame('expected', $result);
}

最初にテストすべきなのは、画面全体ではなく、小さくて重要なロジックです。

金額計算
バリデーション
権限判定
ステータス変更
例外処理
日付計算

慣れてきたら、データプロバイダ、モック、スタブ、Fake、LaravelのFeatureテスト、DBテストへ広げていくとよいです。

PHPのテストコードは、最初は少し面倒に感じるかもしれません。

しかし、テストを書けるようになると、修正後に「壊れていないこと」をすぐ確認できるようになります。

結果として、バグを減らし、リファクタリングしやすくなり、チーム開発でも安心してコードを変更できるようになります。

以上、PHPのテストコードの書き方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次