テスト戦略と実装 - ha1t/php-gm-server GitHub Wiki

テスト戦略と実装

本ページでは、php-gm-serverプロジェクトのテストスイート構成、PHPUnitの設定、各コンポーネントのテスト対象範囲、テスト実行方法、および設計思想について説明します。php-gm-serverはGeminiプロトコル仕様実装に基づいたPHP実装であり、各コアコンポーネントに対して包括的なユニットテストが配置されています。テストスイートは、リクエスト解析、レスポンス構築、静的ファイル配信、TLS証明書生成の4つのコンポーネントをカバーします。

PHPUnit設定

PHPUnitの設定は、プロジェクトルートのphpunit.xmlファイルで管理されます。このファイルは、テストの実行環境、オートローディング設定、テストスイートの定義を含みます。

phpunit.xml の内容:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="default">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

主要な設定ポイント:

設定項目 説明
bootstrap vendor/autoload.php Composerオートローダーを読み込み、PSR-4名前空間マッピングを有効化
colors true テスト実行結果をカラー出力
testsuite default 名前「default」のテストスイートを定義、testsディレクトリ配下すべてのテストを対象

Sources: phpunit.xml

テストスイート構成

4つのテストクラスの概要

php-gm-serverには、4つの主要テストクラスが配置されており、各クラスは対応するコンポーネントのユニットテストを包含します。テストはすべてGeminiServer\Tests名前空間に属し、PSR-4オートロード規約に従ってtests/ディレクトリに配置されています。

graph TD
    A["PHPUnit テストスイート"] --> B["RequestParserTest"]
    A --> C["ResponseBuilderTest"]
    A --> D["StaticFileHandlerTest"]
    A --> E["CertificateGeneratorTest"]
    B --> B1["Geminiスキーム検証"]
    B --> B2["リクエスト長制限"]
    B --> B3["パス解析"]
    C --> C1["ステータス20成功"]
    C --> C2["ステータス51未検出"]
    C --> C3["ステータス59不正"]
    D --> D1["ファイル配信"]
    D --> D2["ディレクトリ処理"]
    D --> D3["MIMEタイプ判定"]
    D --> D4["パストラバーサル防止"]
    E --> E1["証明書生成"]
    E --> E2["既存証明書再利用"]
Loading

RequestParserTest

対象クラス: GeminiServer\RequestParser

RequestParserTestは、Geminiプロトコルのリクエストパース処理に対する6つのテストケースを含みます。このクラスは、クライアントから送信されたGeminiリクエスト文字列(gemini://host/path\r\n形式)をパースし、ホスト名とパスを抽出する責務を持ちます。

テストケース 検証内容 ステータス
testParseValidRequest 標準的なリクエスト形式のパース 通過
testParseRootPath ルートパス(/)のパース 通過
testParseNoTrailingSlash トレーリングスラッシュ不在時の処理 通過
testRejectNonGeminiScheme https://等のスキーム拒否 通過
testRejectTooLongRequest MAX_REQUEST_LENGTH=1024超過時の拒否 通過
testRejectEmptyRequest 空リクエストの拒否 通過

テストコード例:

public function testParseValidRequest(): void
{
    $result = RequestParser::parse("gemini://example.com/hello.gmi\r\n");
    $this->assertSame('example.com', $result['host']);
    $this->assertSame('/hello.gmi', $result['path']);
}

public function testRejectTooLongRequest(): void
{
    $longPath = str_repeat('a', 1024);
    $result = RequestParser::parse("gemini://example.com/{$longPath}\r\n");
    $this->assertNull($result);
}

Sources: RequestParserTest.php:10-50, RequestParser.php:7-48

ResponseBuilderTest

対象クラス: GeminiServer\ResponseBuilder

ResponseBuilderTestは、Geminiレスポンス形式の構築に対する4つのテストケースを含みます。このクラスは、Geminiプロトコル仕様に準拠したSTATUS SPACE META\r\nBODY形式のレスポンスを生成します。

テストケース 検証内容 応答形式
testSuccessResponse 成功レスポンス(ステータス20) 20 {mime}\r\n{body}
testNotFoundResponse 未検出エラー(ステータス51) 51 Not found\r\n
testBadRequestResponse 不正リクエストエラー(ステータス59) 59 {reason}\r\n
testSuccessWithEmptyBody ボディなしの成功レスポンス 20 {mime}\r\n

テストコード例:

public function testSuccessResponse(): void
{
    $response = ResponseBuilder::success('text/gemini', 'Hello Gemini!');
    $this->assertSame("20 text/gemini\r\nHello Gemini!", $response);
}

public function testNotFoundResponse(): void
{
    $response = ResponseBuilder::notFound();
    $this->assertSame("51 Not found\r\n", $response);
}

Sources: ResponseBuilderTest.php:10-35, ResponseBuilder.php:7-23

StaticFileHandlerTest

対象クラス: GeminiServer\StaticFileHandler

StaticFileHandlerTestは、静的ファイル配信に対する6つのテストケースを含みます。このクラスは、ドキュメントルート内のファイルを解決し、MIMEタイプを判定し、ディレクトリアクセス時に自動的にindex.gmiを返却する責務を持ちます。

テストケース 検証内容 結果
testServeExistingFile 既存ファイル配信 MIMEタイプと本文を返却
testServeIndexForDirectory ルートディレクトリへのアクセス index.gmiを自動返却
testServeSubDirectoryIndex サブディレクトリへのアクセス sub/index.gmiを自動返却
testNonGmiMimeType .pngファイルのMIMEタイプ判定 image/pngを返却
testFileNotFound 存在しないファイルへのアクセス nullを返却
testPathTraversalBlocked パストラバーサル攻撃防止 nullを返却(セキュリティ検証)

テストコード例:

public function testServeExistingFile(): void
{
    $handler = new StaticFileHandler($this->docRoot);
    $result = $handler->handle('/about.gmi');
    $this->assertSame('text/gemini', $result['mime']);
    $this->assertSame('# About', $result['body']);
}

public function testPathTraversalBlocked(): void
{
    $handler = new StaticFileHandler($this->docRoot);
    $result = $handler->handle('/../../../etc/passwd');
    $this->assertNull($result);
}

StaticFileHandlerは、セキュリティを重視した設計となっており、realpath()による正規化とドキュメントルート内チェックを実施して、パストラバーサル攻撃を防止します。MIMEタイプマッピングは以下の拡張子に対応しています:

拡張子 MIMEタイプ 拡張子 MIMEタイプ
.gmi, .gemini text/gemini .png image/png
.txt text/plain .jpg, .jpeg image/jpeg
.html text/html .gif image/gif
.css text/css .svg image/svg+xml
.js text/javascript .pdf application/pdf
.json application/json

Sources: StaticFileHandlerTest.php:10-83, StaticFileHandler.php:7-69

CertificateGeneratorTest

対象クラス: GeminiServer\CertificateGenerator

CertificateGeneratorTestは、TLS証明書の生成と再利用に対する2つのテストケースを含みます。このクラスは、OpenSSL拡張を使用して自己署名証明書をオンデマンド生成し、既存の証明書を再利用する機構を実装します。

テストケース 検証内容 動作
testGenerateCreatesCertAndKey 証明書と秘密鍵の生成 PEM形式で保存、BEGIN CERTIFICATE と BEGIN PRIVATE KEY を含む
testGenerateReusesExisting 既存証明書の再利用 同一ホスト名への2回目の呼び出しで同一ファイルを返却

テストコード例:

public function testGenerateCreatesCertAndKey(): void
{
    $generator = new CertificateGenerator($this->certDir);
    $pemPath = $generator->generate('localhost');

    $this->assertFileExists($pemPath);
    $content = file_get_contents($pemPath);
    $this->assertStringContainsString('-----BEGIN CERTIFICATE-----', $content);
    $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $content);
}

public function testGenerateReusesExisting(): void
{
    $generator = new CertificateGenerator($this->certDir);
    $path1 = $generator->generate('localhost');
    $content1 = file_get_contents($path1);

    $path2 = $generator->generate('localhost');
    $content2 = file_get_contents($path2);

    $this->assertSame($content1, $content2);
}

CertificateGeneratorは、各テストケースで一時ディレクトリを生成(setUp())し、テスト後にクリーンアップ(tearDown())する仕組みを采用しており、テスト間の状態汚染を防止します。証明書生成処理の詳細についてはCertificateGeneratorコンポーネントページを参照してください。

Sources: CertificateGeneratorTest.php:10-50, CertificateGenerator.php:7-51

テスト実行方法

全テストの実行

プロジェクトルートから以下のコマンドでテストスイート全体を実行します:

vendor/bin/phpunit

このコマンドは、phpunit.xmlの設定に基づいて、tests/ディレクトリ配下のすべてのテストを検出し実行します。デフォルト設定では、テスト結果がカラー出力で表示されます。

特定のテストクラスの実行

特定のテストクラスのみを実行する場合:

vendor/bin/phpunit tests/RequestParserTest.php
vendor/bin/phpunit tests/ResponseBuilderTest.php
vendor/bin/phpunit tests/StaticFileHandlerTest.php
vendor/bin/phpunit tests/CertificateGeneratorTest.php

特定のテストメソッドの実行

特定のテストメソッドのみを実行する場合:

vendor/bin/phpunit --filter testParseValidRequest
vendor/bin/phpunit --filter CertificateGeneratorTest

出力オプション

PHPUnitの実行時に様々なオプションを組み合わせることが可能です:

オプション 説明
--verbose または -v 詳細な出力を表示
--coverage-html HTML形式のコードカバレッジレポート生成
--testdox テスト構成をマークダウン形式で表示
--stop-on-failure 最初の失敗で中断

Sources: phpunit.xml, composer.json:10-12

テスト結果キャッシュファイル

PHPUnitは、テスト実行結果をキャッシュファイル.phpunit.result.cacheに記録します。このファイルはJSON形式で、テスト実行結果の高速化と再実行判定に活用されます。

キャッシュファイルの構造

{
  "version": 2,
  "defects": {
    "GeminiServer\\Tests\\RequestParserTest::testParseValidRequest": 8,
    ...
  },
  "times": {
    "GeminiServer\\Tests\\RequestParserTest::testParseValidRequest": 0,
    "GeminiServer\\Tests\\CertificateGeneratorTest::testGenerateCreatesCertAndKey": 0.233,
    ...
  }
}

キャッシュ情報の解釈

キー 説明
version キャッシュ形式バージョン(現在は2)
defects テストの実行状態を示す数値(0=通過、8=実行済み)
times 各テストの実行時間を秒単位で記録

現在のキャッシュには18個のテストケースが記録されており、すべてのテストが成功しています。実行時間は以下の通りです:

  • RequestParserTest(6テスト):各0秒
  • ResponseBuilderTest(4テスト):各0秒
  • StaticFileHandlerTest(6テスト):各0秒
  • CertificateGeneratorTest(2テスト):0.233秒、0.289秒(OpenSSL処理による)

Sources: .phpunit.result.cache

テスト駆動設計思想

テスト第一の設計原則

php-gm-serverのテストスイートは、以下の設計思想に基づいています:

  1. コンポーネント単位のユニットテスト

    • 各クラスの責務を明確化し、単一責任原則に基づいたテスト設計
    • 外部依存(ネットワーク、ファイルシステム)の最小化
  2. セキュリティ検証の内在化

    • パストラバーサル攻撃への対策(StaticFileHandlerTest::testPathTraversalBlocked)
    • リクエスト長制限の検証(RequestParserTest::testRejectTooLongRequest)
    • スキーム検証(RequestParserTest::testRejectNonGeminiScheme)
  3. 境界値とエッジケースのカバレッジ

    • 空リクエスト、ルートパスアクセス、非存在ファイル等の処理検証
    • ディレクトリアクセスから自動的にindex.gmiを返却する仕様検証
  4. テスト環境の分離

    • 一時ディレクトリの動的生成(CertificateGeneratorTest、StaticFileHandlerTest)
    • setUp/tearDownメソッドによるテスト前後の初期化・クリーンアップ

設計パターン

テストコード内で採用されている主要なパターン:

パターン 使用例 利点
Arrange-Act-Assert(AAA) すべてのテストメソッド テスト意図の明確化
Setup/Teardown CertificateGeneratorTest、StaticFileHandlerTest テスト間の状態汚染防止
データプロバイダー 現在は未使用だが拡張可能 テストケースの削減

テスト駆動開発(TDD)の適用

php-gm-serverは、Geminiプロトコル仕様に基づいた実装であり、仕様要件がテストの検証基準となっています。各テストケースは、以下の要件から派生しています:

テスト実行フロー

以下は、PHPUnitテスト実行時のフローを示すシーケンス図です:

sequenceDiagram
    participant User as ユーザー
    participant PHPUnit as PHPUnit実行環境
    participant Autoloader as Composer<br/>Autoloader
    participant TestClass as テストクラス
    participant Component as テスト対象<br/>コンポーネント

    User->>PHPUnit: vendor/bin/phpunit
    PHPUnit->>Autoloader: Composerオートローダ初期化
    Autoloader->>Autoloader: PSR-4マッピング適用<br/>GeminiServer\*, Tests\*
    PHPUnit->>PHPUnit: phpunit.xmlから<br/>テストスイート読み込み
    loop テストスイート内のテストクラス
        PHPUnit->>TestClass: setUp()実行
        PHPUnit->>TestClass: テストメソッド実行<br/>(testXxx)
        TestClass->>Component: メソッド呼び出し
        Component-->>TestClass: 結果返却
        TestClass->>TestClass: Assertion実行<br/>(検証)
        PHPUnit->>TestClass: tearDown()実行
    end
    PHPUnit-->>User: テスト実行結果表示<br/>キャッシュ書き込み
Loading

PSR-4オートロード設定

テストクラスと対象コンポーネントの名前空間マッピングは、composer.jsonで定義されています:

{
  "autoload": {
    "psr-4": {
      "GeminiServer\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "GeminiServer\\Tests\\": "tests/"
    }
  }
}

このマッピングにより、GeminiServer\Tests\RequestParserTestクラスは自動的にtests/RequestParserTest.phpから読み込まれ、GeminiServer\RequestParserクラスはsrc/RequestParser.phpから読み込まれます。

Sources: composer.json

Related Pages

⚠️ **GitHub.com Fallback** ⚠️