PHPUnitのモックとスタブの使い方
はじめに
PHPUnitのモックとスタブ学習したので、備忘録としてまとめておく記事です。
PHPUnitのインストールや詳細な使い方については、こちらの記事を参考にしていただければと思います。
この記事で学べること
他のコンポーネントに依存するメソッドやクラスをテストする際、実際のデータや外部環境を使わずに、モックやスタブを使ってテスト用のデータや挙動を再現する方法を学べます。
動作環境
- OS:macOS Sequoia 15.1.1
 - PHP:8.4.4
 - PHPUnit:12.1.4
 - Composer:2.5.1
 
テストダブルとは?
偽物のオブジェクトを使ってテストするというテスト技法の概念です。
作成したメソッドがDBにアクセスする場合に、テスト環境では本物のDBにアクセスしたくない場合など何らかの理由により、メソッドにデータが返ってこない場合に、スタブやモック等を使用して偽物のデータを作成する事ができます。
スタブとは?
スタブは、テスト用データを返すために特化した機能です。テスト時に依存するオブジェクトが実際のデータベースや外部サービスにアクセスする代わりに、決まった戻り値を返すように設定します。これにより、外部のリソースに依存せず、テスト環境を簡素化できます。
スタブは、本物の依存オブジェクトを使うことが難しい場合や、テスト実行環境を整えるのが大変なときに役立ちます。
モックとは?
モックはメソッドの呼び出しに関する振る舞い(回数・引数・順番など)を検証するための仕組みです。
スタブと同様に依存オブジェクトの代わりとして使われますが、主な目的はどう呼ばれたかを検証することにあります。
モックは、特定のメソッドが何回呼ばれたか、どんな引数で呼ばれたか、呼び出しの順序が正しいかを検証するに役立ちます。
スタブでテストしてみる
以下では、スタブを使用してユーザーに関する情報を返す処理をテストする方法を解説します。
- 
Userクラス
ユーザーに関するデータを格納するためのUserクラスを作成します。ユーザーIDと名前を保持するシンプルなクラスです。 
<?php
namespace App\Entity;
class User
{
    public $id;
    public $name;
    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
}
- 
UserRepositoryインターフェース
次に、UserRepositoryというインターフェースを定義します。このインターフェースでは、ユーザーIDでユーザー情報を検索するfindByIdメソッドを定義しています。 
<?php
namespace App\Repository;
use App\Entity\User;
interface UserRepository
{
    public function findById(int $id): User;
}
- 
UserServiceクラス
次に、UserServiceクラスを実装します。このクラスでは、UserRepositoryを使ってユーザー情報を取得し、ユーザー名を返します。 
<?php
namespace App\Service;
use App\Repository\UserRepository;
class UserService
{
    private $userRepository;
    // コンストラクタで UserRepository を受け取る
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    // ユーザーIDからユーザー名を取得
    public function getUserNameById(int $id): string
    {
        $user = $this->userRepository->findById($id);
        return $user->name;
    }
}
- 
スタブを使ったテストケース
UserServiceのgetUserNameByIdメソッドをテストするために、UserRepositoryをスタブとして使います。スタブを使うことで、findByIdメソッドが実際にデータベースにアクセスせず、あらかじめ決められたユーザーデータを返すようにします。 
<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
use App\Repository\UserRepository;
use App\Entity\User;
use App\Service\UserService;
class UserServiceTest extends TestCase
{
    public function testGetUserNameById()
    {
        // スタブを作成
        $userRepositoryStub = $this->createStub(UserRepository::class);
        // findByIdメソッドをスタブ(擬似的にfindByIdメソッドを実装)して、返り値を固定
        $userRepositoryStub->method('findById')->willReturn(new User(1, 'Taro'));
        // UserServiceをテスト対象としてインスタンス化
        $userService = new UserService($userRepositoryStub);
        // getUserNameByIdメソッドを呼び出して、結果が'Taro'であることを確認
        $this->assertSame('Taro', $userService->getUserNameById(1));
    }
}
モックでメソッドの呼び出し回数をテストする
<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
use App\Repository\UserRepository;
use App\Entity\User;
use App\Service\UserService;
class UserServiceTest extends TestCase
{
    public function testGetUserNameByIdCallsFindByIdOnce()
    {
        // モックを作成
        $userRepositoryMock = $this->createMock(UserRepository::class);
        // モックに「findByIdが1回だけ呼ばれること」を期待する
        $userRepositoryMock->expects($this->once())
            ->method('findById')
            ->with(1) // 引数が1であることを確認
            ->willReturn(new User(1, 'Taro'));
        // UserServiceをテスト対象としてインスタンス化
        $userService = new UserService($userRepositoryMock);
        // getUserNameByIdメソッドを呼び出して検証
        $this->assertSame('Taro', $userService->getUserNameById(1));
    }
}
まとめ
たとえば Stripe のように、本番環境でないと実行できない機能のテストには限界があります。
しかしスタブやモックを使えば、外部依存を排除して安全・効率的なテストが可能になります。
こうした手法を学んだことで、より柔軟なテスト設計ができるようになったと感じました。
Discussion