弊社で毎月開催し、PHPエンジニアの間でご好評をいただいているPHPエンジニアのための勉強会
『PHP TechCafe』。2021年9月に開催されたイベントでは「PHPUnit の始め方」について語り合いました。
社外の有識者にも参加頂いてアドバイスを受けながらPHPUnitの使い方やテストコードの書き方を学びました。
今回はその内容についてレポートします。
PHPUnitテストコードの書き方
以下のShowNoteをベースに、「PHPUnit導入の目的」 ~ 「入門にあたり押さえておくべきポイント」などに
ついてディスカッションしました。
以前の『PHP TechCafe』では、PHPUnitのアサーションについて取り上げました。
今回はその続編として、
「アサーションのみならず、テストコード全般について語っていこう!」という趣旨の企画となっております。
まず初めに、「テスト」とは
・品質を担保するための工程
・プログラムが期待通りに動いているかの確認
次に、「ユニットテスト」とは
・単体テスト
・クラスや関数などの単位で動作を確認するテスト
・アプリケーション全体ではなく、アプリケーションを構成する個別のモジュールを対象としたテスト
など、「小さい単位で動作を確認していくテスト」となります。
最後に、「PHPUnit」とは
です。
今回はこの「PHPUnit」をテーマに、「PHPUnit テストコードの書き方【入門】」の記事に沿って
説明が行われていきました。
この記事では、「さいころプログラム」を例に、実際にPHPUnitを使ったテストケースの作成から
テストコードの実行までが簡潔にまとめられています。
setUpメソッド
・各テストメソッドの実行毎に、毎回実行される
・テスト対象としているクラスのインスタンス化や、各テストで利用する共通処理の初期化などの目的で利用
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class StackTest extends TestCase { private $stack; protected function setUp(): void { $this->stack = []; } public function testEmpty(): void { $this->assertTrue(empty($this->stack)); } }
上記サンプルコードの場合、 以下のような処理順となります。
[1] setUpメソッドの実行:配列stackの初期化
[2] testEmptyメソッドの実行:配列stackの初期化チェック
上述の「さいころプログラム」では、 以下のような実装例で説明が行われました。
<?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()); } }
ここで、参加者からの質問が挙がります。
「(インスタンス化は)どのテストでも最初にやらないといけないからここ(setUp)にあるということですよね?」
ここから、「インスタンス化はどこで実行するのがベストなのか!?」という議論が始まります!
・setUpメソッドでインスタンス化するのか!?
・各テストメソッドの中でインスタンス化するのか!?
議論の結果、
・場合によって切り分けるのがベスト!
という結論に至りました。以下の切り分けでよいのではないか という考え方です。
・「入力によってコンストラクタに入れたい」ようなケースであれば各テストメソッドに
・「状態を持たない」ようなケースであればsetUpメソッドに
「setUpの場合はテストケースの実行ごとに毎回呼ばれるので、インスタンスに状態を持っても次のテストケース
には持ち越されない。」 そのため、全部のテストで共通化したい処理をsetUpに書くというよりは、
「あくまでコンストラクタに何か値を直入したいかどうか決めていいだろう」というのが有識者のコメントでした。
参加者はみんな「なるほどなぁ」と納得の様子でした。
このように、サンプルコードに対して参加者からのコメントが入り、そこから活発に議論が展開されて有識者からの貴重なアドバイスが得られるのも『PHP TechCafe』の大きな魅力です!!
ここでの議論はsetUpメソッドだけに留まらず、記事の中では触れられていなかった「setUpBeforeClass」 にも
話題が及びます。ここでもまた、
・重たい初期化や、「次のテストに持ち越す必要がある」場合は一度だけ実行される「setUpBeforeClass」 を
・そうでなければ「setUp」をそのまま利用する
といったアドバイスをいただきました。
アサーション
・値を比較・検査して想定通りの値になっているかを確認する
・テストコードを記述する上で最も重要
assertSame :厳密な型チェックも含めた値の比較を行う
<?php $this->assertSame('hoge', 'hoge'); // OK $this->assertSame('hoge', 'fuga'); // NG $this->assertSame(0, 0); // OK $this->assertSame(0, false); // NG
assertTrue :Trueが返却されることを確認する
<?php $flag = FALSE; $this->assertTrue($flag); $this->assertSame(TRUE, $flag);
「assertSame」メソッドでも同様のケースが記述できるが、テスト結果がわかりやすくなるというメリットがある
<?php # assertTrueのメッセージ Failed asserting that false is true. # assertSameのメッセージ Failed asserting that false is identical to true.
phpunit.readthedocs.io
ここでは、「アサーションは種類も多く、奥が深い」というポイントが、話題に挙がりました。
・テストの目的に応じた適切なアサーションメソッドを選択していく
というのが重要なポイントとなります。
とはいえ、これだけ種類も多く、全てを把握することが困難なことから、
「どれを選択すべきかの判断が難しい」との意見が出ました。
・実行結果も同じになるので、どれを選択すればよいのか分からない
・assertSameで書けないケースはあるのか?
ここでも、有識者からは「 基本はassertSameを使うのがよい 」というアドバイスを頂きました。ただし、以下のような場合はassertEqualsを使うのが良いとのことです。
・assertSameではなくassertEqualsを使うケースの例
- 連想配列で、キーと値の関係はチェックしたいが、 キーの順番は気にする必要がない場合
- インスタンス化されたオブジェクトのpropertyや値は全て一緒で、ハッシュだけが異なる場合 など
また、アサーションがエラーになった時にログへ出力される差分情報の分かりやすさも考慮して
「 assertEqualsの引数の順番に気を付ける必要がある 」というアドバイスも頂きました。
- 第一引数:expected 第二引数:actual となっているため、順番を揃えないと差分が分かりにくくなる
- あらかじめ 期待値は $expected 、テスト対象から取り出した値は $actual という変数に入れておくと分かり易い
データプロバイダ
・テストメソッドへの引数をまとめて記載することができる
・アノテーション @dataProvider を指定して利用する
・配列や、反復が可能な値を返すようにする必要がある
<?php use PHPUnit\Framework\TestCase; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertEquals($expected, $a + $b); } public function additionProvider() { return [ [0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; } } ?>
アノテーション
・各テストメソッドに対するメタ情報
@depends :テストケースの依存性を表す
<?php use PHPUnit\Framework\TestCase; // @depends アノテーションを使った依存性の表現(PHPUnit のドキュメントより転載) class StackTest extends TestCase { public function testEmpty() { $stack = []; $this->assertEmpty($stack); return $stack; } /** * @depends testEmpty */ public function testPush(array $stack) { array_push($stack, 'foo'); $this->assertSame('foo', $stack[count($stack)-1]); $this->assertNotEmpty($stack); return $stack; } /** * @depends testPush */ public function testPop(array $stack) { $this->assertSame('foo', array_pop($stack)); $this->assertEmpty($stack); } }
上記サンプルコードの場合、 以下のように実行が行われます。
[1] testEmptyの実行
[2] testEmptyの実行結果を引数に、testPushを実行
[3] testPushの実行結果を引数に、testPopを実行
「テストの実行結果をもとに、他のテストを実行したい」ようなケースで利用します。
ここでは、「よく使うアノテーションはあるか?」という質問が挙がりました。
いくつか話題に挙がったものをご紹介します。
@runInSeparateProcess :テストを別プロセスで実行する
<?php use PHPUnit\Framework\TestCase; class MyTest extends TestCase { /** * @runInSeparateProcess */ public function testInSeparateProcess() { // ... } }
テストケースの中で グローバルな情報を書き換えても別のテストに影響を及ぼさないようにする ことが可能。
前のテストが後続のテストに悪影響を与え、期待した結果にならないようなケースを回避できる。
@test :テストメソッド先頭の「test」をつけずに、テストメソッドとして実行可能
<?php /** * @test */ public function initialBalanceShouldBe0() { $this->assertSame(0, $this->ba->getBalance()); }
テストメソッドに日本語を利用したい場合など。
参加者の中では @test を使って日本語のメソッド名にする以外に、「test_日本語」 のように先頭の「test」を残して@testを使わずに日本語のメソッド名を使っている人もいるようでした。
モック
・テスト時に実際のオブジェクトの動作をシミュレートしてくれる模造品オブジェクト
・依存するオブジェクトが何らかの理由でテスト時に利用できないときなどに使用
結果の確認方法
すべてOKの場合
$ vendor/bin/phpunit Test.php PHPUnit 7.4.5 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 87 ms, Memory: 4.00 MB OK (4 tests, 10 assertions)
4つのテスト、その中に10個のアサーションがあり、それらすべてがOK
NGがある場合
$ vendor/bin/phpunit Test.php PHPUnit 7.4.5 by Sebastian Bergmann and contributors. ...F 4 / 4 (100%) Time: 78 ms, Memory: 4.00 MB There was 1 failure: 1) Test::testRoll Failed asserting that false is true. /var/www/html/Test.php:40 FAILURES! Tests: 4, Assertions: 10, Failures: 1.
4つのテスト、その中に10個のアサーションがあり、うち1つがNG
テスト実行時に値が変わるケースの実装方法
最後に、「テスト実行時に値が変わるケースはどのようにテストを書くか?」という話題について議論しました。
まずは、「現在時刻などの時刻を扱う場合」 についてです。 ここで挙がった案をご紹介いたします。
・外部から値を注入できるようにしておき、モッククラスで固定の値を返すようにしてパターン網羅する
・標準関数を強制的に上書きすることで、任意の値を返すようにする
・「php-timecop」拡張ライブラリを使い、基準時刻を設定することでdate関数が任意の結果となるようにする
・まだDraftの段階ではあるが、「PSR-20のClockインターフェイス」を実装した時計オブジェクトを用意することで任意の時刻を返す方法もある
上記のように、なかなか個人だけでは思いつかないような案も含めて、様々な実現方法が見つかりました。
次に、 「ランダム値の場合」 です。
ここでも様々な意見が飛び交いましたが、ピックアップしてご紹介します。
・「srand」を使用し、固定のシード値を指定することで同じ結果を得る
・ランダマイザのようなオブジェクトを外出しにし、モックに差し替える
・「srand」を使用すると全体に影響するため、他のテストに依存させたくない時は前述の @runInSeparateProcess を使用する
このように、具体的な実装案を学ぶことができるのも
『PHP TechCafe』の魅力です!!
以上が、今回のイベントテーマに沿った大まかな流れとなります。
イベント参加者からの質問コーナー
イベントの途中で頂いた参加者の皆様からのコメントについて、議論する時間をご用意しています。
ここでいくつかピックアップしてご紹介いたします。
・テスト毎にDBの中身はクリアしてテストデータを投入するか? ・最初からテストデータが入ったDBを使うか?
これについては、テストケース毎にリセットするのがよいという結論に至りました。
具体的には
・「setUpBeforeClass」でTRUNCATEする
・重たい処理でなければ「setUp」で毎回削除する
その理由については以下のような意見がありました。
- DBに依存する処理をモック化しておけば局所的にテストができるので毎回作成しても問題なさそう
- 局所化せずにやっているとテストがむちゃくちゃ重くなってしまう
- 毎回リセットしないと順序に依存したテストを作り込むことになる
・PHPUnit以外を検討したことはあるか?
これについては、スタンダードであり使い慣れたPHPUnitを使いがちという意見が多数でした。
しかし、PHPSpecにはおもしろい機能があり、使いこなせると便利そうといった意見もありました。
BDD(ビヘイビア駆動開発)の機能が備わっているとのことです。
「先にSpecを書いてSpecのための空の実装を自動生成して、それに合うようにテスト実行して、、のように実装とテストを交互に埋めていくようなことが出来る」とのことです。
ただし、「便利に使えたら面白いんですけど、便利に使いこなせないのでPHPUnit使ってます。」
「情報量の多さが違う」ということで、やはりPHPUnitがスタンダードという意見に異論は無いようでした。
『PHP TechCafe』では、イベント参加時の「テーマに関するアンケート」や、
イベント中にも、随時チャットコメントを募集しております。
おわりに
『PHP TechCafe』では今後もPHPに関する様々なテーマのイベントを企画していきます。
是非、皆さまのご参加をお待ちしております!
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
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