ヘルパークラス
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\DomQuery は SimpleXMLElement を継承したクラスで、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);