はじめに
こんにちは、MasaKu です。
ソースコードの修正によりこれまで保証していた動作が保証されず不具合になってしまうケースがあるかと思います。
こういった不具合を減らすためにも手動によるテストに加えて、テストコードによる繰り返しテストを実行できるようにしておくとも重要です。
PHPでは PHPUnit というユニットテストツールを利用することでテストコードを作成することができます。
今回はPHPで作成されたさいころプログラムを例にして PHPUnit のテストコードの書き方をご紹介いたします。
なお、弊社のエンジニアブログにてPHPUnit で利用するアサーションメソッドについて解説された記事がございますので、こちらもあわせてご確認いただけますと幸いです。
PHPUnitのアサーションメソッドを知ろう! - RAKUS Developers Blog | ラクス エンジニアブログ
PHPUnit の基本
PHPUnit とは PHP コードで記述可能なユニットテストツールです。
そのため、普段から PHP のコードを書くプログラマにはとても親しみやすいと思います。
PHPUnit では テストコードを記述するテストクラスを作成
し、そのクラス内で実施したいテストメソッドを追加
していくというのが大まかな流れです。
テストメソッドをどのように作成するかがテストコードを作成する上での重要なポイントかと思いますが、流れとしては以下になります。
単純なテストであれば、対象のプログラムのふるまいを確認したいアサーションを実行するテストメソッドを実装するだけでユニットテストが作成できます。
以下では PHP で作成されたさいころプログラムのテストコードを例にして PHPUnit によるテストコード作成の流れを解説したいと思います。
テスト対象のプログラム
今回作成したさいころプログラムは以下のような構成になっています。
- さいころクラス
- 6面体である
- 1から6までの出目を持っている
- 転がすことで出目が確定する
- 出目を確認することができる
さいころクラスの詳細
<?php // さいころクラス class Dice { protected array $sided; protected int $number; public function __construct (){ } public function setSided() :void{ $this->sided = [1,2,3,4,5,6]; } public function getSided() :array{ return $this->sided; } public function roll() :void{ $this->number = $this->sided[array_rand($this->sided)]; } public function getNumber() :int{ return $this->number; } }
PHPUnit によるテストケース作成
以下では上記のプログラムのテストコードを解説していきます。
なお、PHPUnit のバージョンは 9.5.6
として記載していきます。
まずはざっくりとプログラムを確認していただければと思います。
さいころクラスのテストケース
<?php class DiceTest extends TestCase { protected Dice $dice; protected function setUp() :void{ $this->dice = new Dice(); } public function testInstanceOf() { $this->assertInstanceOf(Dice::class, $this->dice); } public function testEmpty(){ $this->assertTrue(empty($this->dice->sided)); } public function testSided(){ $this->dice->setSided(); $this->assertCount(6, $this->dice->getSided()); $this->assertContains(1, $this->dice->getSided()); $this->assertContains(2, $this->dice->getSided()); $this->assertContains(3, $this->dice->getSided()); $this->assertContains(4, $this->dice->getSided()); $this->assertContains(5, $this->dice->getSided()); $this->assertContains(6, $this->dice->getSided()); return $this->dice; } /** * @depends testSided */ public function testRoll($dice){ $dice->roll(); $this->assertTrue(1 <= $dice->getNumber() && 6 >= $dice->getNumber()); } }
テストクラスの作成方法
まず、テストクラスを作成する方法について解説していきます。
PHPUnit でテストクラスを作成する際にはいくつかルールがあります。
こちら、PHPUnit の公式を参照させていただきます。
Class
という名前のクラスのテストは、ClassTest
という名前のクラスに記述するClassTest
は、(ほとんどの場合)PHPUnit\Framework\TestCase
を継承する
今回のケースだと、Dice
というさいころクラスがテスト対象であるため、テストクラスの名称は DiceTest
という名前になります。
また、2番目のルールとしてPHPUnit\Framework\TestCase
を継承する、というものがあります。これは、PHPUnitでのテスト実行時に必要となる各種メソッドをテストクラス内で利用するために親クラスであるTestCaseから継承したいためです。
(ほとんどの場合)
という注意書きがありますが、TestCase
クラスを継承したテストクラスを継承してテストケースを作成したい、などのシーン以外では、基本的には TestCase
を継承してテストクラスを作成する方法で問題ないと思います。
テストメソッドの作成方法
テストメソッドの作成についても PHPUnit の公式側でルールが決められています。
- テストは、
test*
という名前のパブリックメソッドとなります。- あるいは、
@test アノテーション
をメソッドのコメント部で使用することで、それがテストメソッドであることを示すこともできます。
- あるいは、
アノテーションを付けることも許可されていますが、個人的にはメソッド名の先頭に test
を付けるルールに従う方針で良いのではないかと思います。
この後にも登場しますが、アノテーションはテストコードのメタデータを表す情報となるため、できる限りノイズになるものは少なくした方が良いのではないかと思います。
(全メソッドに @test
というアノテーションを付けるのも無駄かと思います)
テストケースの解説
それでは、テストコードについて解説していきます。
作成したテストコードは以下の通りです
- テスト実施前の準備
- テスト対象がDiceクラスのインスタンスであるかの確認
- さいころの目が意図せず確定していないことの確認
- さいころの目が6面体であり1から6までの出目を持っていることの確認
- さいころを振れば1から6までのいずれかの出目がでることの確認
それぞれについて詳しく解説していきます
setUpメソッド
setUp()
では、PHPUnit にてテストコードを実行する際にはじめに実行されるメソッドです。
今回の例ではテスト対象となる Dice
というクラスのオブジェクトを生成する処理を記載しました。
ここで生成されたオブジェクトを用いて以降のテストケースを実行していきます。
こちらについての詳しい内容は以下のページをご参照ください。
アサーションメソッド
ここから実際にテスト対象のプログラムが、期待通りのふるまいで実装されているかを確認するためのテストコードについて解説していきます。
おさらいになりますが、今回のテスト対象となる Dice
クラスは以下の通りです。
- Diceクラスの特徴
- 6面体である
- 1から6までの出目を持っている
- 転がすことで出目が確定する
- 出目を確認することができる
これらの特徴が必ず保証されていることを確認するためのテストケースを作成していきます。
assertInstanceOf()
assertInstanceOf() は対象となるオブジェクトが指定されたクラスのオブジェクトかどうかを判定するアサーションメソッドです。
setUp()
によりDice
クラスのオブジェクトを生成しているため問題ないと思いますが、オブジェクトが期待通りのものになっていることを確認するようにします。
assertInstanceOf()
の注意点なのですが、指定するオブジェクトのサブクラスのオブジェクトであってもテスト成功と判定されてしまいます。
つまり、Dice
クラスを継承した DummyDice
クラスなどを作成し assertInstanceOf()
を実行した場合、テスト成功となってしまうというわけです。
この動きは PHP の標準機能の instanceOf
という型演算子と同様の動きですのでご注意ください。
assertTrue()
assertTrue() はTrueが返されることを確認するアサーションメソッドです。
Dice
クラスは出目をセットするメソッド(setSideメソッド)を実行するまではどのような出目を持っているかが確定しない仕様になっていますので、PHPの標準関数 empty()
を実行することで、設定されているかを検証することができます。
ちなみに、配列が空であるかどうかをチェックするだけであれば assertEmpty() というアサーションメソッドも存在します。
しかし、PHP標準関数の empty()
では変数が定義されているのか、というところまでチェックできます。
同じアサーションメソッドでもテストコードの記述を工夫することで柔軟にテストケースが書けるのもいいですね。
<?php // 同じ意味のテストケース public function testEmpty(){ // 以下は成功になる $emptyArray = []; $this->assertTrue(empty($emptyArray)); $this->assertEmpty($emptyArray); // 以下は assertEmpty が失敗になる $this->assertTrue(empty($notSetArray)); $this->assertEmpty($notSetArray); }
以下は上記のテスト実行結果です。
PHPUnit 9.5.6 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 00:00.010, Memory: 4.00 MB There was 1 error: 1) PhpUnitTest::testEmpty Undefined variable $notSetArray /home/masaku/study/phpunit/test/PhpUnitTest.php:15 ERRORS! Tests: 1, Assertions: 3, Errors: 1.
assertCount()
assertCount() は指定された配列の要素数が期待通りかどうかを確認するアサーションメソッドです。
こちらも assertSame() というアサーションメソッドを利用することで、同様のテストコードを記述することができます。
<?php // 同じ意味のテストケース public function testSided(){ $this->dice->setSided(); $this->assertCount(6, $this->dice->getSided()); $this->assertSame(6, count($this->dice->sided())); }
assertSame()
を利用しても同じ意味のテストコードが記述できますが、配列の要素数のテストに関しては assertCount()
を利用した方が、テスト失敗時のエラーメッセージがより分かりやすくなる、というメリットがあります。
// asertCount() で失敗した場合のメッセージ 1) DiceTest::testSided Failed asserting that actual size 6 matches expected size 5. // assertSame() で失敗した場合のメッセージ 1) DiceTest::testSided Failed asserting that 6 is identical to 5.
PHPUnit ではアサーションメソッドが豊富ですので、狙い通りのアサーションメソッドが見つかる場合もあるかと思います。しかし、標準関数を併用することで期待するテストコードが記述できますので、あまり見かけないアサーションメソッドを利用してテストコードの可読性を下げてしまうよりは汎用的なアサーションメソッドから実行できるようにする、ということも一定メリットがあるかと思います。
assertContains()
assertContains() は配列内に指定した値を持つ要素が含まれているかを確認するアサーションメソッドです。
少しわかりにくくなってしまいますが、こちらも PHP 標準関数の array_search
を利用すれば assertSame()
と組み合わせることで同様のテストケースが作成可能です。
<?php public function testSided(){ $this->dice->setSided(); $this->assertContains(1, $this->dice->getSided()); $this->assertSame(0, array_search(1, $this->dice->getSided())); }
このテストケースにより 1~6 の数値を持っていることが確認できました。
アノテーション
最後に PHPUnit でのテストコード作成する際、より複雑なテストコードを作成する上で重要になるアノテーションについて解説いたします。
アノテーションとはテストメソッドに対するメタデータを表す構文のことで、PHPDoc などでも利用されています。
PHPUnit ではテストケースの依存性を表現したり、各テストの実行後に毎回実行して欲しい処理などを表現するために利用します。
@depends
@depends というアノテーション はテストケースの依存性を表すアノテーションです。
<?php public function testSided(){ // 省略 return $this->dice; } /** * @depends testSided */ public function testRoll($dice){ $dice->roll(); $this->assertTrue(1 <= $dice->getNumber() && 6 >= $dice->getNumber()); }
testRoll
というさいころを振った際の数値が1から6の間で出現するかどうかを確認するテストケースですが、こちらのテストケースの前提は 1~6 の数値を持った6面体のさいころである
ということが前提になっています。
そのため、上記観点のテストコードを通過できた Dice
クラスのオブジェクトでテストを実施する必要があります。
(6の目しか出ないさいころでもテストが合格になってしまうため)
このような、その他のテストケースの実行後の戻り値を受け取ってテストを実施したい場合はアノテーションを以下のように記述します。
@depends + "テストメソッド名"
このように記述することで、アノテーションが付与されたテストメソッド側で依存している戻り値を引数として受け取ることができます。
これで期待通りのテストケースが実行できます。
テストコードの実行結果
それでは、上記テストをPHPUnitで実行した結果を確認したいと思います。
PHPUnit 9.5.6 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 00:00.010, Memory: 4.00 MB OK (4 tests, 10 assertions)
このように全部で4個のテストメソッド(10個のアサーション)がすべて成功になりました。
もし、テスト対象としている Dice
クラスの出目が全て6のさいころなどに書き換わってしまった場合などは、PHPUnit 側でエラーを検知することができます。
PHPUnit 9.5.6 by Sebastian Bergmann and contributors. ..FS 4 / 4 (100%) Time: 00:00.030, Memory: 4.00 MB There was 1 failure: 1) DiceTest::testSided Failed asserting that an array contains 1. /home/masaki/study/phpunit/test/DiceTest.php:30 FAILURES! Tests: 4, Assertions: 4, Failures: 1, Skipped: 1.
上記の通り、サイコロの出目に1が含まれていないことが確認できています。
おわりに
いかがでしたでしょうか。
PHPUnit はテストコードそのものも PHP のコードで記述できるため、PHPの開発をしている開発者であれば簡単にテストコードが記述できるかと思います。
テストコードが書きたいプログラム自体がテストコードが書きやすい構成になっているか、という課題はありますが、テストコードを書く習慣が無いという方でも導入のイメージが持てるようになっていれば幸いです。
このときはこういう動きをする
ということがある程度固定化されるクラスやメソッドの場合はテストコードを記述しておくことで、コード修正時の不具合を未然に防ぐことができますので、積極的にテストコードを書いていきたいと思います。
参考URL
PHPUnit マニュアル — PHPUnit latest Manual
PHPUnitのアサーションメソッドを知ろう! - RAKUS Developers Blog | ラクス エンジニアブログ
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.comラクスDevelopers登録フォーム
https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/イベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
◆TECH PLAY
techplay.jp
◆connpass
rakus.connpass.com