phpでコードカバレッジを取得しながら手動テストをする方法

## はじめに
手動テストを行いながら、バッググラウンドでカバレッジデータを保存し、後で一括してレポートを生成する方法をまとめました。
プログラムを書いた後、テストをしながら実行していない場所がないかを目で確認することができるようになります。
（本来はユニットテストを書けば良いのですが、いろいろ困難な場合もあるので、手動テストでカバレッジを確認できる仕組みが欲しかった）

* レポートの出力サンプル([php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276579/6e1ed81e-fc54-d800-8af3-1f7b4509f4f4.png)


* CakePHP2で試していますが、環境変数で保存先を指定できますので、環境には依存せず使えるはずです。
  * (環境が少々古いので、最新環境だと多少ソースの変更が必要)


## 動作確認環境
PHPUnitがインストール済みであれば、PHP_CodeCoverageは一緒にインストールされているはずです。

* PHP7.3
* Xdebug3 
* CakePHP2.10
  * PHPUnit5.7
    * PHP_CodeCoverage4.0
    
## 仕組みの説明
* [auto_prepend_file](https://www.php.net/manual/ja/ini.core.php#ini.auto-prepend-file)を利用して、各phpファイル実行前にカバレッジの取得を行うphpファイル(CoverageDumper.php)を挿入する
  * 設定は`.htaccess`に追記
  * 必要があれば「カバレッジデータ」と「レポート」ファイルの出力先も設定する(CakePHP2の場合、デフォルト値で動作するため設定不要)

.htaccess 設定例
```conf
php_value auto_prepend_file /var/www/html/blogTutorial/app/Lib/CoverageDumper.php
```


* カバレッジ取得クラス`CoverageDumper`のコンストラクタで取得開始、デストラクタでカバレッジデータの保存を行う
  * アクセス毎にカバレッジデータが1ファイルずつ増える
  * 要書き込み許可(CakePHP2であれば、/app/tmp)

CoverageDumper.php 抜粋
```php
    function __construct()
    {
        xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
    }

    function __destruct()
    {
        self::initProperty();

        $coverageName = self::$_tmp.'/coverage-' . microtime(true);
        // 先にデータを取得してから停止する(メモリ上のデータを解放するための順)
        $data = xdebug_get_code_coverage();
        xdebug_stop_code_coverage();
        if (self::$_enable) {
            $codecoverageData = json_encode($data);
            file_put_contents($coverageName . '.json', $codecoverageData);
        }
    }
```


* 手動テスト終了後、レポートファイルを生成する
  * 各手動テストの結果をマージして1つにまとめてから、レポートを作成する
  * 要書き込み許可(CakePHP2であれば、/app/webroot)
  * web公開用ディレクトリに作成すれば、そのまま開いて読むことができる

カバレッジレポート生成コントローラー抜粋
```php
App::uses('CoverageDumper', 'Lib');
class  CoverageController extends AppController {
	public function index() {
        error_reporting(0);
        $this->autoRender = false;

        // レポート生成後、ファイルへリダイレクト
        CoverageDumper::createReport();
        $this->redirect('/app/webroot/reports/');
	}
```

* レポートの出力サンプル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276579/6e1ed81e-fc54-d800-8af3-1f7b4509f4f4.png)


## 各ソースファイル

### .htaccess 
保存先を指定する場合、コメントを外して適宜値を変更

```conf
## COVERAGE_TGT_DIR is coverage target directory. default:/app
## COVERAGE_TMP_DIR is to save coverage data(needs write permission). defalut:/app/tmp/
## COVERAGE_DST_DIR is to save coverage report(needs write permission). default:/app/webroot/reports
# SetEnv COVERAGE_TGT_DIR '/var/www/html/blogTutorial/app/Controller'
# SetEnv COVERAGE_TMP_DIR '/var/www/html/blogTutorial/app/tmp/cvg'
# SetEnv COVERAGE_DST_DIR '/var/www/html/blogTutorial/app/webroot/rpt'

php_value auto_prepend_file /var/www/html/blogTutorial/app/Lib/CoverageDumper.php
```


### CoverageDumper.php

```php
<?php
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Report\Html\Facade;

/**
 * CoverageDumper
 * 手動でテスト用、コードカバレッジ取得するクラス(SebastianBergmann\CodeCoverage 4.0.*)
 * 機能
 * * .htaccessの「auto_prepend_file」に指定して、各phpファイル実行時にカバレッジを取得する
 * * 収集したデータをマージしてから、レポートを生成する
 * 補足
 * * データ、レポート保存用に書き込み可能なディレクトリが必要(なければCakePHP2に適した値で作成)
 */
class CoverageDumper
{
    /**
     * カバレッジデータ保存用テンポラリディレクトリ (デフォルト：TMP)
     */
    private static $_tmp;

    /**
     * カバレッジ取得対象ディレクトリ (デフォルト：ROOT.DS.APP_DIR)
     */
    private static $_tgt;
    /**
     * レポート出力対象 (デフォルト：WWW_ROOT.'report')
     */
    private static $_dst;

    /**
     * ディレクトリ存在チェック、書き込みチェックエラーの場合、カバレッジ取得を行わない
     */
    private static $_enable = true;

    /**
     * start code coverage
     */
    function __construct()
    {
        xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
    }

    /**
     * 環境変数から保存先を取得(なければcakephp2に適したデフォルト値)
     */
    private static function initProperty() {
        if (empty(self::$_tmp)) {
            self::$_tmp = getenv('COVERAGE_TMP_DIR')? getenv('COVERAGE_TMP_DIR'): TMP.'coverage';
            self::$_tgt = getenv('COVERAGE_TGT_DIR')? getenv('COVERAGE_TGT_DIR'): ROOT.DS.APP_DIR;
            self::$_dst = getenv('COVERAGE_DST_DIR')? getenv('COVERAGE_DST_DIR'): WWW_ROOT.'reports';

            // 存在チェック＋なければ作成(失敗したらカバレッジ取得しない)
            if (!file_exists(self::$_tmp) && !is_dir(self::$_tmp)) {
                if (!mkdir(self::$_tmp, 0755, true)) {
                    self::$_enable = false;
                }
            }
            if (!file_exists(self::$_dst) && !is_dir(self::$_dst)) {
                if (!mkdir(self::$_dst, 0755, true)) {
                    self::$_enable = false;
                }
            }
        }
    }

    /**
     * save code coverage
     */
    function __destruct()
    {
        self::initProperty();

        $coverageName = self::$_tmp.'/coverage-' . microtime(true);
        // 先にデータを取得してから停止する(メモリ上のデータを解放するための順)
        $data = xdebug_get_code_coverage();
        xdebug_stop_code_coverage();
        if (self::$_enable) {
            $codecoverageData = json_encode($data);
            file_put_contents($coverageName . '.json', $codecoverageData);
        }
    }

    /**
     * create code coverage reports
     */
    public static function createReport() {
        self::initProperty();

        $coverages = glob(self::$_tmp.'/coverage-*.json');

        $codeCoverage = new CodeCoverage();
        $codeCoverage->filter()->addDirectoryToWhitelist(self::$_tgt, ['.php', '.ctp']);

        foreach ($coverages as $index => $coverageFile)
        {
            $codecoverageData = json_decode(file_get_contents($coverageFile), JSON_OBJECT_AS_ARRAY);
            $codeCoverage->append($codecoverageData, $index);
        }

        $report = new Facade();
        $report->process($codeCoverage, self::$_dst);
    }

    /**
     * delete coverage data
     */
    public static function deleteCoverageData() {
        self::initProperty();
        $coverages = glob(self::$_tmp.'/coverage-*.json');
        foreach ($coverages as $val ) {
            unlink($val); // delete file
        }
    }
}

$_coverageDumper = new CoverageDumper();
```

### (参考)レポート生成、表示用コントローラー
CakePHP2で利用する場合のサンプルソース

```php
<?php

App::uses('AppController', 'Controller');
App::uses('CoverageDumper', 'Lib');
/**
 *  Coverage Controller
 *
 */
class  CoverageController extends AppController {
    public function beforeFilter() {
        $this->response->disableCache();
    }
    /**
     * index method
     *
     * @return void
     */
	public function index() {
        error_reporting(0);
        $this->autoRender = false;

        // カバレッジレポート生成
        CoverageDumper::createReport();
        $this->redirect('/app/webroot/reports/'); // 生成したレポートへリダイレクト
	}
}
```