PHPのカバレッジを100%にした話
はじめまして、2月入社の尾宇江です。オーイェーって呼んでほしいです。
すでに、Yappliの新CMSはGo言語でフルリニューアルされているのですが、
一部にはPHPで書かれているリポジトリも残っております。
今回そのリポジトリの中の1つのカバレッジを100%にしたという話をさせていただきます。
対応したリポジトリの内容
- PHP 7.3.5以上 + Laravel 6.x
- アプリからのリクエストを元に外部サービスを実行してレスポンスを返すサービス
- 独自のデータベースは持っていない
- リポジトリ全体のファイル数は1000未満
カバレッジ100%にしたかった理由と方針
理由
入社後のキャッチアップとしてコードリーディングすることになったが、
もともとカバレッジが高いリポジトリだったので、
コードリーディングするついでに100%にしてしまおうという軽い理由。方針
キャッチアップのためのコードリーディングなので、
できるだけ既存コードに変更は加えず、テストコードだけで対応する。
作業内容
事前準備
カバレッジを増加させる前に、まずはphpunitを高速化して計測しやすい環境を作ることにしました。
phpdbgの導入
まず、phpunitの高速化では定番の、phpdbgを導入しました。
導入はとっても簡単で、phpdbgはphp5.6以降だとコアモジュールに入っているので、
# before $ ./vendor/bin/phpunit # after $ phpdbg -qrr ./vendor/bin/phpunit
のように変更するだけです。
簡単ですが効果は絶大で、7分以上かかっていたphpunitの実行が1分台に短縮できました。
phpdbgでコード解析方法が変わる影響ですが、
メソッドの閉じタグの扱いが異なるためにカバレッジ対象となるLinesが変わっただけで、
大きな影響はありませんでした。
codecovの導入
ローカル環境での実行速度には影響ありませんが、 ビルド時の速度向上として、codecovを導入しました。
カバレッジ計測結果をartifactsにアップロードするのに1分近くかかっていたのですが、 codecovへのアップロードは数秒で完了するため、ビルド速度が短縮できました。
対応内容
方針通り、既存コードに大きな修正を掛けずにカバレッジ100%が達成できました。
以下、カバレッジを増加させる際にちょっと面倒だったテストケースを紹介します。
privateメソッド内の条件分岐のテスト
<?php class Target private static function calcTimeout($timeout): int { if ($timeout > 10) { $timeout = 10; } return $timeout; } }
のようなprivateメソッド内の条件分岐も全部カバーしたかったので、
以下のようなテストコードを記載しました。
<?php public function testCalcTimeout(): void { $method = new \ReflectionMethod(Target::class, 'calcTimeout'); $method->setAccessible(true); $result = $method->invoke(null, 1); $this->assertEquals(1, $result, "タイムアウトがうまく設定されなかった"); $result = $method->invoke(null, 11); $this->assertEquals(10, $result, "上限を超えたタイムアウトが設定されてしまった"); }
標準関数の結果で挙動が変わる処理のテスト
標準関数の結果で挙動が変わる処理をテストする場合。
当初は、テスト用の名前空間内で標準関数を上書きしようとしたのですが、テストコードが複雑で見づらくなってしまいました。
そのため、このようなケースだけ、既存コードに手を入れることにしました。
標準関数をラップしただけのメソッドを作成することにより、該当部分のmock化が簡単になりテストコードをシンプルにまとめることができました。
以下、ランダム値のテストをした場合のサンプルです。
<?php class RandomClass { /** * 渡された値と確率で抽選した結果を返す * @param array $parameters * @return string 抽選した */ public function random(array $parameters = []) string { $response = ''; $target_list = []; $total_rate = 0; foreach ($parameters as $key => $value) { if (strpos($key, '_result_') !== false) { $target_list[$key] = (int)$value; $total_rate += (int)$value; } } if ($total_rate > 0) { $pickup = $this->rand(1, $total_rate); foreach ($target_list as $key => $value) { if ($pickup <= $value) { $response = $key; break; } $pickup -= $value; } } return $response; } /** * 乱数を生成する ※rand()のラッパー * @param integer $min 乱数を生成する * @param integer $max 返す値の最大値 * @return integer */ protected function rand(int $min, int $max): int { return rand($min, $max); } }
といったコードに対して、以下のようなテストを追加しました。
<?php public function testRandom(): void { $requestParams = [ 'x' => 1, 'y' => 1, 'z' => 98, ]; // Mockを使ってrand()の結果を固定してしまう $obj = \Mockery::mock(RandomClass::class); $obj->shouldAllowMockingProtectedMethods(); $obj->shouldReceive('random')->passthru(); // randの戻り値を1回目は1、2回目は2、3回目は3に固定 $obj->shouldReceive('rand')->andReturn(1, 2, 3); // 合計3回テスト実施する $result = $obj->random($requestParams); $this->assertEquals($result['value'], 'x', 'rand=1の場合のvalueに想定外の値が設定されている'); $result = $obj->random($requestParams); $this->assertEquals($result['value'], 'y', 'rand=2の場合のvalueに想定外の値が設定されている'); $result = $obj->random($requestParams); $this->assertEquals($result['value'], 'z', 'rand=3の場合のvalueに想定外の値が設定されている'); }
結果
今回は、Mockeryだけで対応できたので、簡単かつ安全にカバレッジを増加させることができました。
また、カバレッジ100%になった結果、 「ミーティングとミーティングの間に5分間スキマ時間ができたけど何しようかな」という時など、気軽にcomposer update
が実行できるようになりました。
三項演算子を使っている箇所があるため、行ベースでのカバレッジ100%であって完全な100%ではありませんが…
今後は、カバレッジが100%になったことを活かして、PHP 7.4 -> PHP 8.0 と最新のPHPに乗り換えていこうと思います!
最後に、テストコードを量産する時の集中スペースとしてお世話になった、私が大好きな社内のオープンスペース"TATAMI"の写真も掲載します!