テストの書き方

Nette Tester のテストを書くことが他にないのは、それぞれのテストが単独で走らせられる PHP のスクリプトだからです。ここには大きな可能性があります。テストを書きながら、そのまま走らせて正しく動くか確かめられます。動かなければ、IDE で簡単に一歩ずつたどって不具合を見つけられます。

テストをブラウザで開くことさえできます。そして何より、走らせることがそのままテストの実行になります。通ったか落ちたかがすぐ分かります。

はじめの章では、配列を扱うごく簡単なテストをお見せしました。今度はテストする自分のクラスを作ります。とはいえこれも簡単なものです。

ライブラリやプロジェクトによくあるディレクトリの構造から始めましょう。大事なのは、テストをほかのコードから分けることです。たとえば配置のためです。テストを本番のサーバーへ上げたくはないからです。構造は次のようになります。

├── src/           # テストする対象のコード
│   ├── Rectangle.php
│   └── ...
├── tests/         # テスト
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

では個々のファイルを作りましょう。まずテストされるクラスを src/Rectangle.php に置きます。

<?php
class Rectangle
{
	private float $width;
	private float $height;

	public function __construct(float $width, float $height)
	{
		if ($width < 0 || $height < 0) {
			throw new InvalidArgumentException('The dimension must not be negative.');
		}
		$this->width = $width;
		$this->height = $height;
	}

	public function getArea(): float
	{
		return $this->width * $this->height;
	}

	public function isSquare(): bool
	{
		return $this->width === $this->height;
	}
}

そしてそのテストを作ります。テストのファイル名は *Test.php*.phpt の形に合わせるべきです。ここでは RectangleTest.php を選びます。

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// ふつうの長方形
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # 期待した結果を確かめます
Assert::false($rect->isSquare());

ご覧のとおり、Assert::same() のようなアサーションのメソッドを使って、実際の値が期待した値と一致することを主張します。

最後の一歩は bootstrap.php ファイルです。ここにはすべてのテストに共通のコード、たとえばクラスの自動読み込み、環境の設定、一時ディレクトリの作成、補助の関数などが入ります。すべてのテストは bootstrap を読み込んでから、テストそのものだけに集中します。bootstrap は次のようになります。

<?php
require __DIR__ . '/vendor/autoload.php';  # Composer のオートローダーを読み込みます

Tester\Environment::setup();               # Nette Tester を準備します

// そしてそのほかの設定(あくまで例で、ここでは必要ありません)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

この bootstrap は、Composer のオートローダーが Rectangle.php のクラスも読み込めることを前提にしています。これはたとえば composer.jsonautoload の区画を設定することなどで実現できます。

これでテストを、ほかの単独の PHP のスクリプトと同じようにコマンドラインから走らせられます。最初の実行で構文の間違いがあれば分かりますし、打ち間違えがなければ次のように出力されます。

$ php RectangleTest.php

OK

テストのアサーションを Assert::same(123, $rect->getArea()); のような誤ったものに変えると、こうなります。

$ php RectangleTest.php

Failed: 200.0 should be 123

in RectangleTest.php(5) Assert::same(123, $rect->getArea());

FAILURE

テストを書くときは、境目の場合をすべて覆うのがよい習慣です。たとえばゼロや負の数のような入力、場面によっては空の文字列や null などです。これはあなたに、そうした場面でコードがどう振る舞うべきかを考えて決めるよう促します。そしてテストがその振る舞いを固めてくれます。

私たちの場合、負の値は例外を投げるべきなので、それを Assert::exception()で確かめます。

// 幅は負であってはいけません
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);

そして高さにも同じようなテストを足します。最後に、両方の寸法が同じなら isSquare()true を返すことをテストします。練習としてそうしたテストを書いてみてください。

見通しのよいテスト

テストのファイルは大きくなり、あっという間に散らかります。ですからテストする領域ごとに別々の関数へまとめると便利です。

まずは、大域の test() 関数を使う、もっと単純でありながら優雅な選択肢を見てみましょう。Tester がこの関数を自動的に作らないのは、あなたのコードに同じ名前の関数があるときの衝突を避けるためです。この関数は setupFunctions() メソッドが作るので、bootstrap.php ファイルで呼んでください。

Tester\Environment::setup();
Tester\Environment::setupFunctions();

この関数を使えば、テストのファイルを名前の付いたまとまりにきれいに組み立てられます。実行すると、その名札が順に出力されます。

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('ふつうの長方形', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('ふつうの正方形', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('寸法は負であってはいけない', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

それぞれの test() の前後でコードを走らせる必要があるなら、それを setUp()tearDown() の関数に渡します。

setUp(function () {
	// それぞれの test() の前に走る準備のコード
});

もうひとつの形はオブジェクト指向のものです。いわゆる TestCase を作ります。これは個々のまとまりが test で始まる名前のメソッドとして表されるクラスです。

class RectangleTest extends Tester\TestCase
{
	public function testGeneralOblong()
	{
		$rect = new Rectangle(10, 20);
		Assert::same(200.0, $rect->getArea());
		Assert::false($rect->isSquare());
	}

	public function testGeneralSquare()
	{
		$rect = new Rectangle(5, 5);
		Assert::same(25.0, $rect->getArea());
		Assert::true($rect->isSquare());
	}

	/** @throws InvalidArgumentException */
	public function testWidthMustNotBeNegative()
	{
		$rect = new Rectangle(-1, 20);
	}

	/** @throws InvalidArgumentException */
	public function testHeightMustNotBeNegative()
	{
		$rect = new Rectangle(10, -1);
	}
}

// テストのメソッドを走らせます
(new RectangleTest)->run();

今回は例外のテストに @throws のアノテーションを使いました。詳しくは TestCaseの章で学べます。

補助の関数

Nette Tester には、テストを楽にするクラスや関数がいくつも入っています。たとえば HTML の文書の内容のテスト、ファイルを扱う関数のテストなどです。

その説明はヘルパークラスのページにあります。

アノテーションとテストの飛ばし方

テストの実行は、ファイルの先頭の phpDoc のコメントのアノテーションで変えられます。たとえば次のようになります。

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

ここに挙げたアノテーションは、このテストを PHP のバージョン 7.2 以上で、しかも pdopdo_pgsql の PHP の拡張がある場合にだけ走らせるべきだと伝えます。これらのアノテーションはコマンドラインのテストの実行器が解釈し、条件が満たされなければテストを飛ばして、出力に s(skipped)の文字で印を付けます。ただしテストを手で走らせるときには、これらのアノテーションは働きません。

アノテーションの説明はテストのアノテーションのページにあります。

テストは Environment::skip() を使って独自の条件で飛ばすこともできます。たとえば次は Windows でテストを飛ばします。

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

ディレクトリの構造

少しでも大きなライブラリやプロジェクトでは、テストのディレクトリを、テストされるクラスの名前空間に沿って下位のディレクトリに分けることをおすすめします。

└── tests/
	├── NamespaceOne/
	│   ├── MyClass.getUsers.phpt
	│   ├── MyClass.setUsers.phpt
	│   └── ...
	│
	├── NamespaceTwo/
	│   ├── MyClass.creating.phpt
	│   ├── MyClass.dropping.phpt
	│   └── ...
	│
	├── bootstrap.php
	└── ...

これでひとつの名前空間、つまり下位のディレクトリのテストだけを走らせられます。

tester tests/NamespaceOne

特別な場面

アサーションのメソッドをひとつも呼ばないテストは疑わしいものと見なされ、エラーとして扱われます。

Error: This test forgets to execute an assertion.

アサーションのないテストが意図してそうなっているなら、Assert::true(true) を呼んでそう印を付けてください。

exit()die() でテストをエラーのメッセージとともに終わらせるのは誤解のもとです。たとえば exit('Error in connection') はテストを終了コード 0 で終わらせるので、成功と受け取られます。代わりに Assert::fail('Error in connection') を使ってください。