PHPの今風なテストフレームワーク「Peridot」の普及活動

## 動機

PHPのテストフレームワークといえば(たぶん)PHPUnitが一番に挙げられると思いますが、歴史が長いだけにテストの書き方も古風というか今風ではないというモヤモヤを感じる今日この頃。

他のテストフレームワークで台頭しているのはPHPSpecとかBehatあたりだと思いますが、普段jsでmochaとchai(最近はpower-assert)を使っている身からするとどちらもしっくりきませんでした。

そんなわけでjsのモダンなテストフレームワークに近い書き方ができるPHPテストフレームワークを探してみたところ、[Peridot](http://peridot-php.github.io/)というものを見つけたのですが、これが思いの外使い勝手が良かったので、紹介と共に普及活動としてスターターキットやGulpプラグインを作ってみました。紹介はのちほど。

ちなみにPHP5.4～対応なのでレガシーなシステムでは導入できません。ご注意を。

## Peridotの紹介

まずはサンプルを見てみましょう。

```php:Peridotサンプル
describe('ArrayObject', function() {
  beforeEach(function() {
    $this->arrayObject = new ArrayObject(['one', 'two', 'three']);
  });

  describe('->count()', function() {
    it("should return the number of items", function() {
      $count = $this->arrayObject->count();
      assert($count === 3, "expected 3");
    });
  });
});
```

見るからに今風なBDDスタイルですね。パッと見でjsと勘違いしてしまうんじゃないかと思うほどです(言い過ぎ)。

このようにjsとphpでほぼ同じようなテストの書き方ができるというのがイチオシな点です。

もちろんテストの書き方だけでなく、Peridotには他にも面白い機能が実装されています。

### テストの保留/解除

まだ未実装で通らないテストを一時的に保留させる方法です。

```php:pendingテスト
xdescribe("A pending suite", function() {
  xcontext("when using a pending context", function() {
    xit("should have a pending spec", function() {

    });
  });
});
```

`describe`や`context`、`it`の頭に`x`が付けられています。このように頭に`x`が付いたテストは実行されず、pending扱いされます。pendingとなったテストの数も表示されます。

![pending.png](https://qiita-image-store.s3.amazonaws.com/0/17021/0dda4236-b016-a627-e725-d9d6663f0a47.png)

コメントアウトして一時的テストを保留させると、コメントアウトしたことを忘れてしまって結局テストされず、なんてことになりかねませんが、このように頭の`x`のトグルだけでテストの保留を切り替えられ、なおかつそれがテスト時に表示されるのはなかなか便利な機能ではないでしょうか。

### アサーションライブラリ「Leo」

Peridotと同じ作者さんが公開している[Leo](http://peridot-php.github.io/leo/)というアサーションライブラリがあります(Peridotとは別でインストールが必要)。こちらを組み合わせるとよりモダン感溢れるテストを書けるようになります。Expect、Assertインターフェイスを実装しています。

jsでいえばchaiに近い感覚で使用することができます。

```php:Expectインターフェイス
describe('test hoge', function() {
  it('should have 3 items', function() {
    expect([1,2,3])->to->have->length(3);
  });
});
```

```php:Assertインターフェイス
use Peridot\Leo\Interfaces\Assert;

describe('test hoge', function() {
  it('should have 3 items', function() {
    $assert = new Assert();
    $assert->equal(count([1,2,3]), 3);
  });
});
```

Assertインターフェイスの場合はテストの度に`new`するか、`$this->assert`みたいな感じで保持しておく必要があるのでちょっと面倒ですが、ラッパー関数を作ればそれほどストレスはたまりません。

```php:こんな感じ
use Peridot\Leo\Interfaces\Assert;

function ass() { return new Assert(); }
describe('test hoge', function() {
  it('should have 3 items', function() {
    ass()->equal(count([1,2,3]), 3);
  });
});
```

このようにPeridotとLeoを組み合わせることによってjsでいうmochaとchaiを使った時とほぼ同じ感覚でPHPのテストを書くことができます。

### 各種カスタマイズ

Peridot自体はPHPUnitほど機能が豊富なわけではありません。テスト結果の表示形式(reporter)も1種類しかありません。しかし設定ファイルやプラグインによる拡張で、かなりのカスタマイズができるようになっています。

#### 設定ファイル:peridot.php

Peridotによるテスト実行時にはデフォルトでカレントディレクトリにある__peridot.php__が読み込まれます(あれば)。コマンドラインオプションで設定ファイルのパスを指定することもできます。

設定ファイルは以下のように書きます。

```php:peridot.php
use Evenement\EventEmitterInterface;
use Peridot\Core\Test;

return function(EventEmitterInterface $emitter) {
  $emitter->on('test.failed', function (Test $test) {
    //log the failure?
    //abort abort abort?
    //let your loved ones know about your shortcomings?
  });
}
```

`EventEmitterInterface`を引数とする関数を返すのが設定ファイルのルールとなっています。

上の例では`test.failed`、つまりテストが失敗したタイミングでPeridot本体から呼び出される関数を`$emitter->on()`で登録しています。

イベントベースで処理を書くのもなんかjsっぽい感じがしますね。

で、拡張っつってもイベント発生時に何すればいいのよ？ってことですが、

たとえばコードカバレッジ機能を組み込みたい場合は、`peridot.start`時に`PHP_CodeCoverage`を`new`して`peridot.reporters`でカバレッジ結果を出力する、みたいな。

他にも`suite.start`や`test.passed`などいろいろなイベントが用意されています(イベントによって渡される引数の種類は異なるのでご注意を)。詳しくは[公式ドキュメント](http://peridot-php.github.io/events.html)をご覧ください。

#### 独自コマンドラインオプションの追加

さらにPeridotでは独自のコマンドラインオプションを登録することができます。

デフォルトでは以下のような感じですが、

```sh:デフォルト
$ peridot --help
Usage:
 peridot [options] [files]

Options:
 --grep (-g)          Run tests matching <pattern> (default: *.spec.php)
 --no-colors (-C)     Disable output colors
 --reporter (-r)      Select which reporter to use (default: spec)
 --bail (-b)          Stop on failure
 --configuration (-c) A php file containing peridot configuration
 --reporters          List all available reporters
 --version (-V)       Display the Peridot version number
 --help (-h)          Display this help message.
```

コマンドラインオプションを追加することによってこんな感じにすることができます。コードカバレッジ関係の拡張をした時の例です。

```sh:独自拡張
$ peridot --help
Usage:
 peridot [options] [files]

Options:
 --grep (-g)               Run tests matching <pattern> (default: *.spec.php)
 --no-colors (-C)          Disable output colors
 --reporter (-r)           Select which reporter to use (default: spec)
 --bail (-b)               Stop on failure
 --configuration (-c)      A php file containing peridot configuration
 --reporters               List all available reporters
 --version (-V)            Display the Peridot version number
 --help (-h)               Display this help message.
 --coverage-html           Code coverage(HTML) report directory
 --coverage-xml            Code coverage(XML) report directory
 --coverage-clover         Code coverage(Clover) report file
 --coverage-php            Code coverage(PHP) report file
 --coverage-crap4j         Code coverage(Crap4j) report file
 --coverage-text           Code coverage(Text) report file
 --coverage-blacklist (-B) Blacklist file/dir for Code coverage (multiple values allowed)
 --coverage-whitelist (-W) Whitelist file/dir for Code coverage (multiple values allowed)
```

コマンドラインオプションの追加は以下のように行います。

```php:コマンドラインオプションの追加
$emitter->on('peridot.start', function (Environment $env, Application $app) {
  $env->getDefinition()->option(
    'hoge',                       // --hogeオプションとして登録される
    'H',                          // --hoge = -Hとなる
    InputOption::VALUE_REQUIRED,  // --hoge=fugaみたいに値の指定が必須
    'example option hoge'         // --help時に表示されるオプション説明
  );
}
```

で、追加した`--hoge`オプションが指定されてPeridotが起動したらそのオプションを受け取ってゴニョゴニョするという流れです。コマンドラインオプションの受け取り方はこんな感じです。

```php:コマンドラインオプションを受け取る
$emitter->on('peridot.execute', function (InputInterface $input, OutputInterface $output) {
  $hoge = $input->getOption('hoge');
}
```

しかし、こんな感じにコマンドラインオプションを登録してコマンドラインオプションを受け取って、その値を元に云々・・・というようなことを書いていくとあっという間に設定ファイルが肥大化してしまいます。

#### カスタマイズのプラグイン化

そこで、このように大量の記述が必要な設定はプラグイン(外部ライブラリ)化して、設定ファイルではプラグインをロードする形にするとスッキリさを保てます。

```php:プラグイン読み込み
require('plugin-hoge.php');

use Evenement\EventEmitterInterface;
use MyPlugin\Hoge;

return function(EventEmitterInterface $emitter) {
  new Hoge($emitter); // この中で各種イベント処理やコマンドラインオプションの登録など
};
```

プラグインの書き方ですが、ベースとなるクラスもないのでこれといって決まりはなさそうですが、後述する公式のプラグインのソースを~~パク~~参考にすると良いと思います。

#### その他

ここまで紹介した以外にも[Scope](http://peridot-php.github.io/scopes.html)や[DSL](http://peridot-php.github.io/syntax.html)のカスタマイズ・拡張といったことが可能なようですが、この辺りはちゃんと調べていないので紹介のみにとどめておきます。

#### 公式プラグイン

というかまだ知名度が低いフレームワークなので有志のプラグインというものはほとんどありません。

<https://github.com/peridot-php>で見つけたよさげなプラグインを簡単に紹介します。

##### [peridot-dot-reporter](https://github.com/peridot-php/peridot-dot-reporter)

1つのテスト結果を1ドットで表示するrepoterを使用できるようにするプラグインです(画像は公式より拝借)。

![dot.png](https://qiita-image-store.s3.amazonaws.com/0/17021/aaa680f7-e254-5023-2960-5964a319faa3.png)

##### [peridot-list-reporter](https://github.com/peridot-php/peridot-list-reporter)

1つのテスト結果を1行で表示するrepoterを使用できるようにするプラグインです(画像は公式より拝借)。

![list.png](https://qiita-image-store.s3.amazonaws.com/0/17021/686634d3-78e5-0cf9-c699-9b22abfa9ee9.png)

##### [peridot-code-coverage-reporters](https://github.com/peridot-php/peridot-code-coverage-reporters)

コードカバレッジの結果を出力するreporterを使用できるようにするプラグインです。とはいえ自分はこのプラグインを発見する前に自前でコードカバレッジプラグインを作ってしまっていたので使ったことはありません。

##### [peridot-yo-plugin](https://github.com/peridot-php/peridot-yo-plugin)

一昔前に流行ったYoでYoできるプラグインのようです。自分はYoをYoく知らないのでこれが便利なものなのかネタなのかは分かりません。

##### [peridot-watcher-plugin](https://github.com/peridot-php/peridot-watcher-plugin)

最近のビルドツールでは当たり前となっているファイル変更監視機能ですが、Peridotの場合は単体でこの機能をプラグインで導入することができます(画像は公式より拝借)。

![watcher.gif](https://qiita-image-store.s3.amazonaws.com/0/17021/1e191fbf-be13-9195-b4bc-7693f5589e03.gif)

##### [leo-http-foundation](https://github.com/peridot-php/leo-http-foundation)

PeridotではなくアサーションライブラリのLeoを拡張して、http関連のアサーションメソッドを追加するプラグインです。

##### [peridot-dsl-example](https://github.com/peridot-php/peridot-dsl-example)

こちらはプラグインではありませんが、DSLのカスタマイズのサンプルです。DSLを拡張してこんなことができるよー的なやつです。

##### [peridot-scope-example](https://github.com/peridot-php/peridot-scope-example)

同じくScopeのカスタマイズのサンプルです。

## 普及活動1: この記事

やっぱり日本語の記事があると入りやすさが違うよね！ということでこの記事をしたためております。

## 普及活動2: スターターキット

[ktty1220/peridot-starter-kit](https://github.com/ktty1220/peridot-starter-kit)

Peridotによるテストを簡単に始めることができるスタータキットを作成しました。

`src`ディレクトリにテストしたいモジュールが入っていて、`specs`ディレクトリ内のテストスクリプトを実行するという流れです。インストールや実行の仕方は[README.md](https://github.com/ktty1220/peridot-starter-kit/blob/master/README.md)をご覧ください。

なお、`peridot-easy`ディレクトリには自作のプラグインなどが入っています。簡単にカスタマイズ内容を紹介します。

### コードカバレッジ機能

コマンドラインオプションでコードカバレッジの指定やブラックリスト/ホワイトリストの登録ができるようになります。

```sh:コードカバレッジ指定例
$ vendor/bin/peridot --coverage-html report -B peridot-easy -B peridot.php -B foo -B bar.php
```

### テスト結果を顔文字で表示するreporter追加

![face.png](https://qiita-image-store.s3.amazonaws.com/0/17021/e12ccc3a-407c-d811-84e5-b8b83be5d807.png)

ふざけてません(真顔)。いや何というかデフォルトのreporterはWindows7環境でテスト結果のシンボルが文字化けするのが気に入らなかったのでASCII文字だけでテスト結果を表現しようとしたらこうなってました。

```sh
$ peridot -r face
```

で、このふざけた顔文字reporterを使用できます。

あと、このreporterはエラー時のスタックトレースを排出しません。デフォルトのreporterだとスタックトレースが多すぎてエラー情報がスクロールアウトしてしまうことと、排出されるスタックトレースのほとんどがPeridot内部に関するものなのでテスト時には邪魔なケースの方が多かったというのが理由です。

## 普及活動3: Gulpプラグイン

[ktty1220/gulp-peridot](https://github.com/ktty1220/gulp-peridot)

Peridot自体にWatcherプラグインがあるので別にGulpに組み込まなくてもいいんですが、オプションの指定とかも楽だし、gulpfile.jsで一元管理したい欲求とかが襲ってきたので作りました。

```sh
$ npm install --save-dev gulp-peridot
```

でインストールできます。

このプラグインには上で紹介したスターターキットの拡張機能が組み込まれているので、簡単にコードカバレッジ機能や~~ふざけた~~お茶目な顔文字reporterを使用することができます。

こちらも[README.md](https://github.com/ktty1220/gulp-peridot/blob/master/README.md)や[example](https://github.com/ktty1220/gulp-peridot/tree/master/example)などを参考にしてください。

ちなみにこのプラグインは[gulp-phpunit](https://www.npmjs.com/package/gulp-phpunit/)の使い方やオプションを可能な限り踏襲しているので、gulp-phpunitからの乗り換えも簡単にできるようになっています。

## まとめ

自由にカスタマイズできたりプラグインがいろいろあるとはいえ、それでもPHPUnitがデフォルトで提供している機能(アノテーションとかモックとか)には及ばないとは思います。しかし、そもそも自分の場合はPHPUnitの豊富な機能をほとんど使ってなかったので、PeridotとLeoのモダンなテスト記法と、足りない部分をプラグインやカスタマイズで補強していくスタイルが気に入りました。

しかしマイナー故に開発停滞→自然消滅みたいなことがあっては勿体ないので、こうして普及活動をしてみました。この記事を見てPeridotキニナルという方が増えれば我が生涯に一片の悔いなしです(言い過ぎ)。