FuelPHP でユニットテスト + カバレッジ取得

FuelPHP でユニットテストを行い、カバレッジも取得してみた。

# 導入

今回は AWS EC2 上に環境を構築した。

* PHP 7
* MySQL 5.7
* Nginx
* FuelPHP 1.8
* PHPUnit 5.5
* AspectMock 1.0

## PHP7 と Nginx と MySQL5.7 のインストール

```bash
# nginx
$ sudo yum install -y nginx

# php7
$ sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
$ sudo yum --disablerepo=* --enablerepo=remi install -y php70 php70-php-fpm php70-php-devel php70-php-mbstring php70-php-pdo php70-php-mysql php70-php-mysqlnd php70-php-pecl-xdebug

# mysql
$ sudo rpm -ihv http://dev.mysql.com/get/mysql57-community-release-el6-7.noarch.rpm
$ sudo yum install -y mysql-server
```

PHP7 をインストールするときに、 **--disablerepo=\*** を指定しなかったら、
amzn リポジトリと remi リポジトリが混じって入ってしまった。
このせいで、やり直す羽目に。  :sob: 

※ xdebug はカバレッジを取得するために必要。

### PHPバージョン確認

```bash
# Path を通す
# ~/.bash_profile の末尾にも追加しておく
$ source /opt/remi/php70/enable

# バージョン確認
$ php -v
PHP 7.0.11 (cli) (built: Sep 14 2016 07:27:30) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies
    with Xdebug v2.4.1, Copyright (c) 2002-2016, by Derick Rethans
```

xdebug もインストールされていることを確認できた。

### サービス起動

Nginx と MySQL と FPM (FastCGI Process Manager) を起動する。

```bash
# FPM
$ sudo service php70-php-fpm start
Starting php-fpm:                                          [  OK  ]

# Nginx
$ sudo service nginx start
Starting nginx:                                            [  OK  ]

# MySQL
$ sudo service mysqld start
Initializing MySQL database:                               [  OK  ]
Installing validate password plugin:                       [  OK  ]
Starting mysqld:                                           [  OK  ]
```

サーバが起動した際に自動的にサービスが開始するようにしておく。

```bash
$ sudo chkconfig nginx on
$ sudo chkconfig php70-php-fpm on
$ sudo chkconfig mysqld on
```

### MySQLバージョン確認

```bash
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.15, for Linux (x86_64) using  EditLine wrapper
```

## FuelPHP のインストール

ドキュメントに従ってクイックインストール！

```bash
# oil を Web からクイックインストールします
$ curl get.fuelphp.com/oil | sh
```

### プロジェクトを作成

作成したいディレクトリへ移動し、以下のコマンドを実行。

```bash
# sample プロジェクト作成
$ oil create sample

# FuelPHP がアクセスできるように書き込み権限を与える
$ oil refine install
        Made writable: APPPATH/cache
        Made writable: APPPATH/logs
        Made writable: APPPATH/tmp
        Made writable: APPPATH/config
```

### Nginx の設定

[FuelPHP のサイト](http://fuelphp.jp/docs/1.8/installation/instructions.html#/nginx) を参考に /etc/ngix/nginx.conf を編集する。

### FuelPHP の確認

Document Root にアクセスして Welcome ページが表示されれば、無事インストール完了！

![welcome.png](https://qiita-image-store.s3.amazonaws.com/0/134613/1429abaa-6272-ba81-0f0e-2a609c7dbd2a.png)


## PHPUnit と AspectMock のインストール

AspectMock は普通の mocking framework ではないって[AspectMock のサイト](https://github.com/Codeception/AspectMock)に書いてあるし、
「[普通じゃないモッキングフレームワークAspectMockがパワフル過ぎる](http://blog.a-way-out.net/blog/2014/12/10/aspect-mock/)」でもパワフル過ぎるって書いてあるので使ってみる。

まずは、プロジェクト・ディレクトリに移動して、composer.json に追加。

```json:composer.json
    "require-dev": {
        "phpunit/phpunit": "5.5.*",
        "codeception/aspect-mock": "1.*"
    },
```

codeception/aspect-mock のバージョンを `"*"` にすると `2.*` がインストールされるが、
実行時に「Uncaught exception 'InvalidArgumentException' 」が発生するので、
**バージョンを `1.*` に落としてインストール。**

```bash
$ php composer.phar update
```

このままでは PHPUnit への Path が通っていないので、oil.php で Path を設定する。

```bash
# oil.php を app/config へコピーする
cp fuel/packages/oil/config/oil.php fuel/app/config/.
```

```php:fuel/app/config/oil.php
  // Path を設定
  'autoload_path' => '' ,
  'binary_path'   => DOCROOT.'fuel/vendor/bin/phpunit' ,
```

AspectMock を入れると oil test で例外（Exception: Serialization of 'Closure' is not allowed）
が発生するので、phpunit.xml を修正する。

```bash
# phpunit.xml をコピーする
$ cp fuel/core/phpunit.xml fuel/app/.
```

```xml:fuel/app/phpunit.xml
<!--
<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php">
-->
<phpunit colors="true" stopOnFailure="false" bootstrap="../core/bootstrap_phpunit.php" backupGlobals="false">
```

早速 Core モジュールをテストしてみる。

```bash
$ oil test --group=Core
Tests Running...This may take a few moments.
PHPUnit 5.5.5 by Sebastian Bergmann and contributors.

...............................................................  63 / 402 ( 15%)
............................................................... 126 / 402 ( 31%)
............................................................... 189 / 402 ( 47%)
F.............................................................. 252 / 402 ( 62%)
............................................................... 315 / 402 ( 78%)
............................................................... 378 / 402 ( 94%)
........................                                        402 / 402 (100%)

Time: 744 ms, Memory: 14.00MB

There was 1 failure:

1) Fuel\Core\Test_Format::test_to_xml_boolean with data set #0 (array(array('Value 1', 35, true, false)), '<?xml version="1.0" encoding=.../xml>\n', '<?xml version="1.0" encoding=.../xml>\n', '<?xml version="1.0" encoding=.../xml>\n', '<?xml version="1.0" encoding=.../xml>\n')
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
 '<?xml version="1.0" encoding="utf-8"?>
-<xml><item><field1>Value 1</field1><field2>35</field2><field3>1</field3><field4></field4></item></xml>
+<xml><item><field1>Value 1</field1><field2>35</field2><field3>1</field3><field4/></item></xml>
 '

/home/ec2-user/tmp/sample/fuel/core/tests/format.php:425

FAILURES!
Tests: 402, Assertions: 495, Failures: 1.
```

なんと、私の環境ではテストが１つ失敗してしまった！！！
どうも libxml のバージョン絡みらしいが、ちょっと調べた程度では解決できなかった。
今のところ支障がないので、このまま放置。　♪～(￣ε￣；) 

# ユニットテスト

やっと環境が整ったので、テストケースを作成していく。

## テスト対象の作成
まずは DB の接続設定を行う。（DB sample は作成しておくこと）

```php:fuel/app/config/development/db.php
return array(
    'default' => array(
        'connection'  => array(
            'dsn'        => 'mysql:host=localhost;dbname=sample',
            'username'   => '********',
            'password'   => '********',
        ),
    ),
);
```

次にテスト対象をスキャフォールドで作成する。
[Generate - Oil パッケージ - FuelPHP ドキュメント　#スキャフォールド](http://fuelphp.jp/docs/1.8/packages/oil/generate.html#scaffolding)

```bash
# scaffold
$ oil g scaffold monkey name:string description:text
        Creating migration: APPPATH/migrations/001_create_monkeys.php
        Creating model: APPPATH/classes/model/monkey.php
        Creating controller: APPPATH/classes/controller/monkey.php
        Creating view: APPPATH/views/monkey/index.php
        Creating view: APPPATH/views/monkey/view.php
        Creating view: APPPATH/views/monkey/create.php
        Creating view: APPPATH/views/monkey/edit.php
        Creating view: APPPATH/views/monkey/_form.php
        Creating view: APPPATH/views/template.php

# migrate
$ oil refine migrate
Performed migrations for app:default:
001_create_monkeys
```

テーブルが作成されたか確認する。

```bash:mysql
mysql> use sample
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+------------------+
| Tables_in_sample |
+------------------+
| migration        |
| monkeys          |
+------------------+
2 rows in set (0.00 sec)

mysql>
```

このままでは、 Orm パッケージが読み込まれないので config.php の以下のコメントを外す。

```php:fuel/app/config/config.php
    // 'always_load'  => array(
（中略）
        // 'packages'  => array(
        //  //'orm',
        // ),
（中略）
    // ),
```

ブラウザでアクセスしてみる。
![monkeys_01.png](https://qiita-image-store.s3.amazonaws.com/0/134613/f7bad5ad-2f0a-07be-36d8-7a693c59d4aa.png)

何故 monkey なのかわからないが。。。とりあえず上手くいった。
この後のテストのためにデータを１件登録しておく。
![monkeys_02.png](https://qiita-image-store.s3.amazonaws.com/0/134613/d934f37f-b283-18b3-881f-385936deb8ab.png)

## カバレッジ取得設定

カバレッジを取得するために phpunit.xml に設定を追加。
[PHPUnit マニュアル － 付録C XML 設定ファイル](https://phpunit.de/manual/current/ja/appendixes.configuration.html)

```xml:fuel/app/phpunit.xml
    <logging>
        <log type="coverage-html" target="../app/logs/test/coverage"
             lowUpperBound="35" highLowerBound="70"/>
    </logging>
    <filter>
        <whitelist addUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">../app</directory>
            <exclude>
                <directory suffix=".php">../app/tests</directory>
                <directory suffix=".php">../app/vendor</directory>
                <file>../app/bootstrap.php</file>
            </exclude>
        </whitelist>
    </filter>
```

## Model のテスト

最初に Model のテストケースを作成してみる。
validate メソッドしかないけどね。

```php:fuel/app/tests/model/monkey.php
<?php
/**
 * Model Monkey class tests
 *
 * @group App
 * @group Model
 */
class Test_Model_Monkey extends TestCase
{
    public function test_validate_成功()
    {
        $val = Model_Monkey::validate('Test_Model_Monkey::test_validate_成功');

        // name も description もあり
        $actual = $val->run(
            [
                'name'          =>  '孫悟空',
                'description'   => 'ドラゴンボールの主人公'
            ]);
        $this->assertTrue($actual);
    }

    public function test_validate_失敗()
    {
        // Validation オブジェクト取得
        $val = Model_Monkey::validate('Test_Model_Monkey::test_validate_失敗');

        // name も description もなし
        $actual = $val->run([]);
        $this->assertFalse($actual);

        // name なし
        $actual = $val->run(['description' => 'ドラゴンボールの主人公']);
        $this->assertFalse($actual);

        // description なし
        $actual = $val->run(['name' => '孫悟空']);
        $this->assertFalse($actual);
    }
}
```

テストを実行。

```bash
$ oil test --group=App
Tests Running...This may take a few moments.
PHPUnit 5.5.5 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 338 ms, Memory: 14.00MB

OK (2 tests, 4 assertions)

Generating code coverage report in HTML format ... done
```

さっそくカバレッジを見てみる。
出力されたカバレッジをブラウザから見えるように nginx.conf に location を追加する。

```conf:/etc/nginx/nginx.conf
location /coverage/ {
    alias   /○○○○○○/sample/fuel/app/logs/test/coverage/;
}
```

app
![coverage_app.png](https://qiita-image-store.s3.amazonaws.com/0/134613/47eba671-a4c9-1ad4-39a9-01e9d7a670d4.png)

app/classes
![coverage_app_classes.png](https://qiita-image-store.s3.amazonaws.com/0/134613/33e70894-6fd0-549c-f90b-b86c4fb40eea.png)

model
![coverage_app_classes_model.png](https://qiita-image-store.s3.amazonaws.com/0/134613/69ce2e8c-9629-51c6-b3d5-7abff15f423e.png)

Model_Monkey
![coverage_app_classes_model_monkey.png](https://qiita-image-store.s3.amazonaws.com/0/134613/dc2f2d61-a20d-bc16-ef16-5a4802538192.png)

validate メソッドが **100%** になってることが確認できた。

## Controller のテスト

簡単そうな **action_view メソッド** をテストしてみる。

Controller は Model に依存しているので、Model をモック化（テストダブルに）してテストする。
Model_Monkey::find() は **static method ですが AspectMock なら大丈夫！**

```php:fuel/app/tests/controller/monkey.php
<?php

use AspectMock\Test as test;

/**
 * Controller Monkey class tests
 *
 * @group App
 * @group Controller
 */
class Test_Controller_Monkey extends TestCase
{
    protected function tearDown()
    {
        test::clean();  // remove all registered test doubles
    }

    public function test_action_view_成功()
    {
        $expected =
            [
                'name'          =>  '孫悟空',
                'description'   => 'ドラゴンボールの主人公'
            ];

        // mock
        $model = test::double('Model_Monkey', ['find' => new Model_Monkey($expected)]);

        $response = Request::forge('monkey/view/2')
            ->set_method('GET')
            ->execute()
            ->response();

        $model->verifyInvokedOnce('find', [2]);

        $this->assertEquals(200, $response->status);
        $this->assertEquals('Monkey', $response->body->title);

        $actual = $response->body->content->monkey;

        $this->assertEquals($expected['name'], $actual['name']);
        $this->assertEquals($expected['description'], $actual['description']);
    }
}
```

テストを実行してカバレッジを確認。

```bash
$ oil test --group=App
Tests Running...This may take a few moments.
PHPUnit 5.5.5 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 389 ms, Memory: 14.00MB

OK (3 tests, 8 assertions)

Generating code coverage report in HTML format ... done
```

app/classes
![coverage_app_classes02.png](https://qiita-image-store.s3.amazonaws.com/0/134613/8c73525b-797a-8c3c-843f-eff531cf28e3.png)

controller
![coverage_app_classes_controller.png](https://qiita-image-store.s3.amazonaws.com/0/134613/31dc1418-bbe6-b0c1-8f59-8e56f73d4ee3.png)

Controller_Monkey
![coverage_app_classes_controller_monkey.png](https://qiita-image-store.s3.amazonaws.com/0/134613/2b84b502-fbea-e112-8b08-f5d9bffdec31.png)

**ちゃんとカバレッジが取れている！！！**

## Dashboard ？？？

カバレッジページに Dashboard っていうリンクがあったので見てみたら、
循環的複雑度（Cyclomatic complexity）なんかが表示されていた。

![coverage_app_classes_dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/134613/214c3dc1-916b-6f97-2ddb-feb82bfb07e6.png)

この中に **CRAP** っていうものが表示されているけど、何かわからなかったので調べてみたら、
PHPUnit のマニュアルに載っていた。
[コードカバレッジの指標](https://phpunit.de/manual/current/ja/code-coverage-analysis.html#code-coverage-analysis.metrics)

> **Change Risk Anti-Patterns (CRAP) Index**
　Change Risk Anti-Patterns (CRAP) インデックス とは、循環的複雑度と、
　あるコード単位のコードカバレッジに基づいて算出される指標です。 複雑度が低く、
　適切なテストカバレッジが達成されているコードは、CRAPインデックスの値が
　低くなります。
　CRAPインデックスを下げるには、テストを書くか、 あるいはリファクタリングで
　コードの複雑性を下げます。

だそうです。
へぇ～
PHPUnit もなかなかやりますね！

# まとめ
Unit Test をするとき、使える Mocking Framework がないとやる気がでない。（気が萎える）
PHP でも AspectMock というパワフルなものがあったので本当に良かった。