nekoroll.hatenablog.com
これの続き
そもそもvoidとして受け取ることは無理だった
https://www.php.net/manual/ja/migration71.new-features.php#migration71.new-features.void-functionsvoid という型の値はないので無理ですね、有効な意味のある値でないことを示すための値としては null があります。なんで null でない無効値が欲しいんです?
— sji (@sji_ch) 2020年8月18日
void
ではなくnull
になる
そもそもやりたかったのは
- assertionがなくてriskyテストになるのを避けたかった
doesNotPerformAssetions
だとearly return削った時にテストがコケなくて怖い- どうしよう…
という感じだった
じゃあどう対処するべきか?を悩んでいた所、色々教えてもらった
sji先生やLS先生に教えてもらった
LS先生
関数型プログラミングを志すなら関数は戻り値を返しつづけ、状態変化させる関数は絶滅させなければならない。そうした方がテストは確かにしやすい。
— LS (@LS_CiAL) 2020年8月17日
処理の中身にもよるけど、どうしても早期リターンが必要なら何も起きないことをテストするしかないかな!
— LS (@LS_CiAL) 2020年8月17日
sji先生
どのように処理が行われたか / 行われなかったかを呼び出し元から検知できること&そのようにできることのテストが必要なのであれば、返り値なりで通知する必要がある気はします(そのテストが必要なら本当にそれは void と宣言されてるのが正しいのか、という)
— sji (@sji_ch) 2020年8月18日
「呼び出しによって何らかの副作用が発生するけど、呼び出し時点では呼び出し元へその副作用発生の有無は通知されないよ」というのが本当にその関数の仕様な場合、テストとしては別口で副作用またはモック利用の有無を確認する、くらいしかやれることないんじゃないかな、と
— sji (@sji_ch) 2020年8月18日
以上を踏まえて何をやるべきか
まずは対象となるコード
<?php class Hoge { public function fuga(int $id): void { if (1 === $id) { // early return return; } // なんかいい感じにデータベース操作できるやつ $db = new ORM(); // なんか適当にDBインサート処理とか処理する $db->insert($id); } }
※例が適当すぎてよくなかったので、DBアクセスはインスタンス生成して処理するようにした
これに対してテストを書く
対応できそうなアプローチは以下
- $dbをMockにして呼ばれないことを担保する
- Mockにできないなら処理を呼び出すプロキシ的なメソッドを用意してMockにする
- そもそも処理内容をテストする必要がある(呼び出し側から検知する必要がある)場合、
void
ではなくbool
なり何かを返すべき
が適切そうだった
DB操作をMockにする
まずMockにしやすいようにDIパターンを適用する
<?php class Hoge { // なんかいい感じにデータベース操作できるやつ private $db = ORM; public function __construct(ORM $db) { $this->db = $db; } public function fuga(int $id): void { if (1 === $id) { // early return return; } // なんか適当にDBインサート処理とか処理する $this->db->insert($id); } }
呼び出されないことを検証する
<?php class Hoge { use PHPUnit\Framework\TestCase; /** *@test */ public function idが1の時は処理しないこと() { // モック化して呼ばれないことを検証 $db_mock = \Mockery::mock(ORM::class); $db_mock->shouldNotReceive('insert'); $hoge = new Hoge($db_mock); $hoge->fuga(1); // PHPUnitとしてはassertionが無いと怒るので必要 $this->doesNotPerformAssertions(); } }
これでearly returnが削除された場合shouldNotReceive('insert')
で検出できるようになった
モックにしたいけど出来ない場合
プロキシ的なメソッドを用意する
<?php class Hoge { public function fuga(int $id): void { if (1 === $id) { // early return return; } // プロキシメソッドを介して呼ぶようにする $this->proxyMethod($id); } public function proxyMethod($id): void { // なんかいい感じにデータベース操作できるやつ $db = new ORM(); // なんか適当にDBインサート処理とか処理する $db->insert($id); } }
呼び出されないことを検証する
<?php class Hoge { use PHPUnit\Framework\TestCase; /** *@test */ public function idが1の時は処理しないこと() { // 一部のみモック化して検証したいのでパーシャル化 $hoge_mock = \Mockery::mock(Hoge::class)->makePartial(); $hoge_mock->shouldNotReceive('proxyMethod'); $hoge_mock->fuga(1); // PHPUnitとしてはassertionが無いと怒るので必要 $this->doesNotPerformAssertions(); } }
同様にshouldNotReceive('proxyMethod')
で検出できるようになった
※パーシャルの扱いイマイチだけど多分これでいいはず
戻り値を与える
voidからboolに変更
<?php class Hoge { public function fuga(int $id): bool { if (1 === $id) { // インサートしていない を示すfalse return false; } $db = new ORM(); // インサートに成功した場合boolを返すと仮定 return $db->insert($id); } }
戻り値があるのでテストが書ける
<?php class Hoge { use PHPUnit\Framework\TestCase; /** *@test */ public function idが1の時は処理しないこと() { $hoge = new Hoge($db_mock); $result = $hoge->fuga(1); // インサートされていないこと(falseであること)を検証できる $this->assertFalse($result); } }
まとめ
- mockにするのが個人的にはベスト。テスタビリティ高いしわかりやすい
- mockが無理ならプロキシメソッドを用意して呼び出す手法もあり
- 処理内の実行結果を検証したい場合
void
は誤りかもしれない- ただし
void
が必ずしも誤りというわけではない
例としてバリデーション処理でエラーなら例外、OKならvoidとなったりもする
- ただし
doesNotPerformAssetions
は結局必要になる
コメントを添えておくとテストコードとしてとても優しいと思う ラブアンドピース
バリデーション処理の例外の場合も結局expectException
で検証しつつ
正常系はdoesNotPerformAssetions
を付ける形になりそう
いろいろ考えて、DIパターンにしつつ全部publicにしていいんじゃないかな…と思った
呼び出される範囲考えてprivateにしてたけど、public/protectedでも良い気がする
でも可視性は適切につけるべきだから、プロキシメソッドを用意するのがベストなのかな
これもうわかんねぇな、お前どう?