Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

PHPカバレッジ100%の世界

PHPのカバレッジを100%にした話

はじめまして、2月入社の尾宇江です。オーイェーって呼んでほしいです。
すでに、Yappliの新CMSはGo言語でフルリニューアルされているのですが、
一部にはPHPで書かれているリポジトリも残っております。
今回そのリポジトリの中の1つのカバレッジを100%にしたという話をさせていただきます。

f:id:OhYeah:20200917114619p:plain
記念すべきカバレッジ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"の写真も掲載します!

f:id:OhYeah:20200918100609j:plain
Yappliのオープンスペース"TATAMI"