Skip to content

07 Testing Guide

KITO, takayuki edited this page Aug 13, 2026 · 1 revision

7. テスト戦略 (Testing Guide)

Woof が「純粋関数」というアーキテクチャを採用している最大の理由は、Web アプリケーションのテストを劇的に簡単かつ確実なものにするためです。

一般的な Web フレームワークでは、Controller や View のテストを書くために複雑なモックライブラリ (Mockery など) を導入したり、ヘッドレスブラウザで HTML の DOM を解析したりする必要があります。しかし Woof では、そうした外部ツールへの依存を最小限に抑え、PHP 標準の PHPUnit だけで極めて堅牢な単体テスト (Unit Test) を記述できます。

この章では、Woof ならではのテスト手法を解説します。

1. 時間と乱数のモック化 (決定論的なテスト)

テストを実行するたびに現在時刻や乱数の結果が変わってしまうと、「有効期限切れ」や「確率の分岐」のテストを正確に検証することができません。 Woof では、 Environment にテスト用の特殊なクラスを注入することで、これらの副作用を完全に固定 (コントロール) できます。

FixedClock (時間の固定)

任意の日時 (Unix タイムスタンプ) を指定して時間を固定します。これにより、深夜 0 時をまたぐ処理などの境界値テストが容易になります。

ArrayRandom (乱数の固定)

配列で指定した数値を順番に返す特殊な Random クラスです。これにより、ランダムに分岐するはずの処理を 100% 予測可能な状態にできます。

use PHPUnit\Framework\TestCase;
use Woof\Web\WebEnvironmentBuilder;
use Woof\System\FixedClock;
use Woof\System\ArrayRandom;

class GameControllerTest extends TestCase
{
    public function testGameLogicWithFixedTimeAndRandom()
    {
        // 1. 時間と乱数を完全に固定した環境を構築
        $clock      = new FixedClock(1700000000);
        $random     = new ArrayRandom([5, 1, 3]); // 呼ばれるたびに 5, 1, 3 を順に返す
        $envBuilder = new WebEnvironmentBuilder();
        $env        = $envBuilder
            ->setConfigDir(__DIR__ . "/config")
            ->setResourcesDir(__DIR__ . "/resources")
            ->setClock($clock)
            ->setRandom($random)
            ->build();

        // 以降、この $env を Controller に渡すことで、
        // 常に同じ時間、同じサイコロの目が出る状態でテストを実行できます。
    }
}

2. View の純粋な単体テスト

Woof の View には「純粋な値」しか渡されないため、データベースやリクエストの状態を気にすることなく、純粋な Input (コンストラクタ引数) と Output (レンダリング結果の文字列) の検証を行うことができます。

use PHPUnit\Framework\TestCase;
use Woof\Locale;
// ※ Context や Resources のテスト用モック / スタブの生成は省略しています

class TimeViewTest extends TestCase
{
    public function testRenderJapaneseGreeting()
    {
        // 1. 入力となる純粋な値を用意
        $locale   = new Locale("ja-JP");
        $time     = "2024/01/01 12:00:00";
        $greeting = "はじめまして。";

        // 2. View の構築
        $view     = new TimeView($locale, $time, $greeting);

        // 3. レンダリングの実行
        $html     = $view->render($resources, $context);

        // 4. アサーション (期待される文字列が含まれているか)
        $this->assertStringContainsString("<h1>現在の時刻</h1>", $html);
        $this->assertStringContainsString("<p>はじめまして。</p>", $html);
        $this->assertStringContainsString("<p>現在時刻は 2024/01/01 12:00:00 です。</p>", $html);
    }
}

3. Controller の単体テスト (DOM 解析の排除)

一般的な Controller のテストでは、出力された HTML をパースして「画面に期待する値が表示されているか」を検証しがちです。しかし、これではデザイナーが HTML のタグ (DOM 構造) を少し変更しただけでテストが壊れてしまいます (脆いテスト) 。

Woof の Controller の役割は「HTML を作ること」ではなく、「適切な View と状態をレスポンスにセットすること」です。したがって、テストコードでは レスポンスから View オブジェクトを取り出し、その内部状態 (プロパティ) を直接検証 します。

※ これを実現するため、テスト対象の View クラスにはテスト用の Getter メソッド (例: getGreeting() ) を用意しておくのがベストプラクティスです。

use PHPUnit\Framework\TestCase;
use Woof\Http\Request;
use Woof\Web\WebEnvironmentBuilder;
use Woof\System\FixedClock;
use Woof\Web\ViewBody;
// (他、必要なクラスの use は省略)

class TimeControllerTest extends TestCase
{
    public function testFirstVisitReturnsFirstMessage()
    {
        // 1. 環境のセットアップ
        $clock      = new FixedClock(1700000000);
        $envBuilder = new WebEnvironmentBuilder();
        $env        = $envBuilder
            ->setConfigDir(__DIR__ . "/config")
            ->setResourcesDir(__DIR__ . "/resources")
            ->setClock($clock)
            ->build();
        
        // 2. 初回訪問をシミュレートした Request の構築 (Cookie なし)
        $request    = new Request(/* ... */);

        // 3. Controller の実行
        $controller = new TimeController();
        $response   = $controller->handle($request, $env);

        // 4. HTTP レスポンスのアサーション (ステータスコードや Cookie)
        $this->assertSame(200, $response->getStatus()->getCode());
        
        $cookie = $response->getCookie("last_visited");
        $this->assertNotNull($cookie);
        $this->assertSame("1700000000", $cookie->getValue());

        // 5. View のアサーション (HTML のパースは行わない)
        $body = $response->getBody();
        
        // Operator::setView() によってセットされたボディが ViewBody であることを確認
        $this->assertInstanceOf(ViewBody::class, $body);
        
        // ViewBody から実際の View インスタンスを取り出して型を確認
        $view = $body->getView();
        $this->assertInstanceOf(TimeView::class, $view);
        
        // TimeView に正しい状態 (純粋な値) がセットされているかを検証
        $this->assertSame("はじめまして。", $view->getGreeting());
        $this->assertSame("2023/11/14 22:13:20", $view->getTime()); // 固定された時間に基づく
    }
}

このように、Controller は「正しい状態を View に渡せたか」をテストし、View は「渡された状態を正しく HTML に描画できるか」をテストすることで、関心事が完全に分離された壊れにくい強固なテストスイートを構築することができます。

Clone this wiki locally