テスト戦略と実装 - ha1t/php-gm-server GitHub Wiki
本ページでは、php-gm-serverプロジェクトのテストスイート構成、PHPUnitの設定、各コンポーネントのテスト対象範囲、テスト実行方法、および設計思想について説明します。php-gm-serverはGeminiプロトコル仕様実装に基づいたPHP実装であり、各コアコンポーネントに対して包括的なユニットテストが配置されています。テストスイートは、リクエスト解析、レスポンス構築、静的ファイル配信、TLS証明書生成の4つのコンポーネントをカバーします。
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
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["既存証明書再利用"]
対象クラス: 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
対象クラス: 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
対象クラス: 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
対象クラス: 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 CertificateGeneratorTestPHPUnitの実行時に様々なオプションを組み合わせることが可能です:
| オプション | 説明 |
|---|---|
--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のテストスイートは、以下の設計思想に基づいています:
-
コンポーネント単位のユニットテスト
- 各クラスの責務を明確化し、単一責任原則に基づいたテスト設計
- 外部依存(ネットワーク、ファイルシステム)の最小化
-
セキュリティ検証の内在化
- パストラバーサル攻撃への対策(StaticFileHandlerTest::testPathTraversalBlocked)
- リクエスト長制限の検証(RequestParserTest::testRejectTooLongRequest)
- スキーム検証(RequestParserTest::testRejectNonGeminiScheme)
-
境界値とエッジケースのカバレッジ
- 空リクエスト、ルートパスアクセス、非存在ファイル等の処理検証
- ディレクトリアクセスから自動的に
index.gmiを返却する仕様検証
-
テスト環境の分離
- 一時ディレクトリの動的生成(CertificateGeneratorTest、StaticFileHandlerTest)
- setUp/tearDownメソッドによるテスト前後の初期化・クリーンアップ
テストコード内で採用されている主要なパターン:
| パターン | 使用例 | 利点 |
|---|---|---|
| Arrange-Act-Assert(AAA) | すべてのテストメソッド | テスト意図の明確化 |
| Setup/Teardown | CertificateGeneratorTest、StaticFileHandlerTest | テスト間の状態汚染防止 |
| データプロバイダー | 現在は未使用だが拡張可能 | テストケースの削減 |
php-gm-serverは、Geminiプロトコル仕様に基づいた実装であり、仕様要件がテストの検証基準となっています。各テストケースは、以下の要件から派生しています:
- Geminiプロトコル仕様実装 のリクエスト形式、レスポンス形式仕様
- セキュリティ設計 のパストラバーサル防止要件
- ファイル配信システム詳細 のMIMEタイプ判定仕様
以下は、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/>キャッシュ書き込み
テストクラスと対象コンポーネントの名前空間マッピングは、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
- プロジェクト概要 — プロジェクトの目的と技術スタックの概要
- Geminiプロトコル仕様実装 — テスト仕様の基盤となるプロトコル定義
- RequestParserコンポーネント — RequestParserクラスの実装詳細
- ResponseBuilderコンポーネント — ResponseBuilderクラスの実装詳細
- StaticFileHandlerコンポーネント — StaticFileHandlerクラスの実装詳細
- CertificateGeneratorコンポーネント — CertificateGeneratorクラスの実装詳細
- セキュリティ設計 — セキュリティ検証の設計思想
- ファイル配信システム詳細 — MIMEタイプ判定とディレクトリ処理の詳細