phpUnit を使用したメソッドの自動テスト
acms-google-calendar の開発に phpUnit を使用してみました。各メソッドが期待通りの動作を行うかどうか、期待する入力と出力を定義するだけで判定できるので、とても便利でした。定義した入出力のペアに対しては、バグがないことを保証でき、ソフトウェアの信頼性を高められたのではないかと思います。また、リファクタリングについても、動いていた処理を壊してしまう心配を低減できました。ここでは、phpUnit の導入から、acms-google-calendar でのテストについて記します。
導入
phpUnitを導入したいプロジェクトで次のコマンドを実行します。
composer require phpunit/phpunit --dev
テスト記述
testフォルダを作成し、"test"と名前のついた拡張子が.phpのファイルを作成します。
作成したファイルに、例えば次のように記します。これは acms-google-calendar の AddHour メソッドをテストします。
<?php use PHPUnit\Framework\TestCase; require_once dirname(__FILE__)."/../Engine.php"; class AddDateTimeTest extends TestCase { /** * create Engin instance without constructor */ public function setUp() { $this->engin = new DummyClass("Acms\Plugins\GoogleCalendar\Engine"); } public function testAddHour() { $response = $this->engin->addDateTime("2020-7-8 10:00:00", "0-0-0 1:0:0"); $this->assertEquals("2020-07-08 11:00:00", $response); $response = $this->engin->addDateTime("2020-7-8 10:00:00", "00-00-00 01:00:00"); $this->assertEquals("2020-07-08 11:00:00", $response); } public function testAddMin() { $response = $this->engin->addDateTime("2020-7-8 10:00:00", "00-00-00 01:20:40"); $this->assertEquals("2020-07-08 11:20:40", $response); } public function testAddYear() { $response = $this->engin->addDateTime("2020-7-8 10:00:00", "10-10-10 01:00:00"); $this->assertEquals("2031-05-18 11:00:00", $response); } public function testAddedDateSpreadsAcrossTwodays() { $response = $this->engin->addDateTime("2020-7-8 10:00:00", "00-00-00 20:00:00"); $this->assertEquals("2020-07-09 06:00:00", $response); } }
テスト実行
次のコマンドでテストを実行します。--color はテスト結果に色を付ける引数です。test/ は先ほど作成した test ディレクトリを表す相対パスです。
./vendor/bin/phpunit test/ --color
補足
先ほどのテストプログラム中のDummy クラスについて説明します。
acmes-google-calendar の Engin クラスに存在する addHour メソッドはプライベートです。そのため、外部から直接テストすることはできません。php にはリフレクションクラスという、外部からプライベートメソッドにアクセスするための仕組みが用意されています。ただ一般的な方法では、リフレクションクラスを作成し、インスタンス化した際に、コンストラクタメソッドが実行されます。Engin クラスのコンストラクタメソッドには、引数として、フォームidなど、モックとして用意しにくい変数などが含まれています。そのためコンストラクタを実行せずインスタンス化する工夫も行っています。
class DummyClass { public function __construct($className) { $this->reflection = new ReflectionClass($className); // コンストラクタなしで ReflectionClass をインスタンス化 $this->instance = $this->reflection->newInstanceWithoutConstructor(); } public function __call($name, $arguments) { $method = $this->reflection->getMethod($name); $method->setAccessible(true); return $method->invokeArgs($this->instance, $arguments); } }