ヘルパークラス

HttpAssert

Tester\HttpAssert クラスは HTTP のサーバーをテストする道具を提供します。HTTP のリクエストを簡単に行い、状態コード、ヘッダー、応答の本文の内容を、つなげて書ける API で確かめられます。

# 基本の HTTP のリクエストと応答の確認
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
	->expectCode(200)
	->expectHeader('Content-Type', contains: 'json')
	->expectBody(contains: 'users');

fetch() メソッドは既定で GET のリクエストを作りますが、すべてのパラメータを変えられます。

HttpAssert::fetch(
	'https://api.example.com/users',
	method: 'POST',
	headers: [
		'Authorization' => 'Bearer token123',  # 連想配列
		'Accept: application/json',            # あるいは文字列の形式
	],
	cookies: ['session' => 'abc123'],
	follow: false,                             # リダイレクトを追いません
	body: '{"name": "John"}'
)
	->expectCode(201);

状態コードは expectCode()denyCode() のメソッドで確かめられます。具体的な数も、検証の関数も渡せます。

$response
	->expectCode(200)                           # ちょうどそのコード
	->expectCode(fn($code) => $code < 400)      # 独自の検証
	->denyCode(404)                             # 404 であってはいけません
	->denyCode(fn($code) => $code >= 500);      # サーバーのエラーであってはいけません

ヘッダーの確認には expectHeader()denyHeader() のメソッドを使います。ヘッダーがあるか、その値がちょうど一致するか、内容の一部が合うかを調べられます。

$response
	->expectHeader('Content-Type')                     # ヘッダーがなければなりません
	->expectHeader('Content-Type', 'application/json') # ちょうどその値
	->expectHeader('Content-Type', contains: 'json')   # その文を含みます
	->expectHeader('Server', matches: 'nginx %a%')     # その形に合います
	->denyHeader('X-Powered-By')                       # ヘッダーがあってはいけません
	->denyHeader('X-Debug', contains: 'sensitive')     # その文を含んでいてはいけません
	->denyHeader('X-Debug', matches: '~debug~i');      # その形に合ってはいけません

応答の本文の確認も、expectBody()denyBody() のメソッドで同じように働きます。

$response
	->expectBody('OK')                              # ちょうどその値
	->expectBody(contains: '"status": "success"')   # JSON の断片を含みます
	->expectBody(matches: '%A% hello %A%')          # その形に合います
	->expectBody(fn($body) => json_decode($body) !== null) # 独自の検証
	->denyBody('Error occurred')                    # ちょうどその値であってはいけません
	->denyBody(contains: 'error')                   # その文を含んでいてはいけません
	->denyBody(matches: '~exception|fatal~i');      # その形に合ってはいけません

follow のパラメータは、HttpAssert が HTTP のリダイレクトをどう扱うかを決めます。

# リダイレクトを追わずにテストします(既定)
HttpAssert::fetch('https://example.com/redirect', follow: false)
	->expectCode(301)
	->expectHeader('Location', 'https://example.com/new-url');

# すべてのリダイレクトを追って最後の応答まで進みます
HttpAssert::fetch('https://example.com/redirect', follow: true)
	->expectCode(200)
	->expectBody(contains: 'final content');

DomQuery

Tester\DomQuerySimpleXMLElement を継承したクラスで、CSS のセレクタを使って HTML や XML を簡単に検索できます。

# HTML の文字列から DomQuery を作ります
$dom = Tester\DomQuery::fromHtml('
	<article class="post">
		<h1>Title</h1>
		<div class="content">Text</div>
	</article>
');

# CSS のセレクタで要素があるかをテストします
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));

# 要素を DomQuery オブジェクトの配列として探します
$headings = $dom->find('h1');
Assert::same('Title', (string) $headings[0]);

# 要素がセレクタに合うかをテストします(バージョン 2.5.3 以降)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));

# セレクタに合うもっとも近い祖先を探します(2.5.5 以降)
$article = $content->closest('.post');
Assert::true($article->matches('article'));

XML の文書には fromXml() メソッドを使います。

$dom = Tester\DomQuery::fromXml('<catalog><item>First</item></catalog>');
Assert::true($dom->has('item'));

FileMock

Tester\FileMock はメモリの中でファイルを真似て、fopen()file_get_contents()parse_ini_file() などの関数を使うコードのテストを楽にします。使い方の例です。

# テストされるクラス
class Logger
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
	}
}

# 新しい空のファイル
$file = Tester\FileMock::create('');

$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');

# 作られた中身をテストします
Assert::same("Login\nLogout\n", file_get_contents($file));

省略できる第 2 パラメータ $extension は、生成される URL のファイルの拡張子を決めます。テストされるコードがそれをもとに判断する場合に便利です。

$file = Tester\FileMock::create('{"key": "value"}', 'json');

Assert::with()

これはアサーションではなく、オブジェクトの private のメソッドやプロパティをテストするための助っ人です。

class Entity
{
	private $enabled;
	// ...
}

$ent = new Entity;

Assert::with($ent, function () {
	Assert::true($this->enabled); // private の $ent->enabled にアクセスできます
});

Helpers::purge()

purge() メソッドは指定したディレクトリを作り、すでにあればその中身をすべて消します。一時ディレクトリを作るのに役立ちます。たとえば tests/bootstrap.php で使います。

@mkdir(__DIR__ . '/tmp');  # @ - ディレクトリはすでにあるかもしれません

define('TempDir', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TempDir);

Environment::lock()

テストは並行して走ります。とはいえテストの実行が重ならないようにしたいこともあります。たいていはデータベースのテストで、データベースの中身を用意し、実行中にほかのテストがデータベースに手を出さないようにする必要があります。そうしたテストでは Tester\Environment::lock($name, $dir) を使います。

Tester\Environment::lock('database', __DIR__ . '/tmp');

第 1 パラメータは錠の名前、第 2 パラメータは錠を保存するディレクトリへのパスです。最初に錠を取ったテストが進み、ほかのテストはそれが終わるのを待ちます。

Environment::bypassFinals()

final と印を付けられたクラスやメソッドはテストしにくいものです。テストの先頭で Tester\Environment::bypassFinals() を呼ぶと、コードの読み込みのときに final のキーワードが取り除かれます。

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass はもう final ではありません
{
	// ...
}

Environment::setup()

  • エラーのダンプを読みやすくします(色付けも含みます)。そうでなければ PHP の既定のスタックトレースが出力されます
  • テストでアサーションが呼ばれたかの確認を有効にします。そうでなければ、アサーションのないテスト(たとえば書き忘れたもの)も通ってしまいます
  • 実行されたコードの情報の収集を自動的に始めます(--coverage を使ったとき)(あとで説明します)
  • スクリプトの終わりに OK か FAILURE の状態を出力します

Environment::setupFunctions()

大域の関数 test()testException()testNoError()setUp()tearDown() を作ります。これらでテストを組み立てられます。

test('テストの説明', function () {
	Assert::same(123, foo());
	Assert::false(bar());
	// ...
});

Environment::VariableRunner

テストが直接走らされたのか、Tester 経由で走らされたのかを判断できます。

if (getenv(Tester\Environment::VariableRunner)) {
	# Tester が走らせました
} else {
	# ほかの方法で走らされました
}

Environment::VariableThread

Tester は指定した数のスレッドでテストを並行して走らせます。スレッドの番号を知りたいなら、環境変数から得られます。

echo "Running in thread number " . getenv(Tester\Environment::VariableThread);