PHP Advent Calendar 2013 in Adventar の3日目です。
前日は @matsubo さんの コピペで出来るComposer導入 でした。Composer、私も使ってます。
Grunt とは Node.js で作られた色々な作業を自動化するためのツールです。 普通は Node.js での開発や js とかのフロントエンド開発に使われますが、PHP での開発でもわりと便利です。
PHP のための Grunt プラグインも色々あるので、とりあえず次の2つだけ使ってみます。
前提
php や node や npm はあらかじめインストールしておいてください。
ソースとテストの準備
とりあえず phpunit が実行できるソースツリーを用意します。phpunit は今風に composer でインストールします。
$ find . -type f ./src/Sample.php ./tests/phpunit.xml.dist ./tests/bootstrap.php ./tests/SampleTest.php ./composer.json
composer.json
{ "autoload": { "psr-0": { "": "src/" } }, "require-dev": { "phpunit/phpunit": "3.7.*" } }
Sample.php
<?php class Sample { public function add($a, $b) { // @todo 未実装 } }
bootstrap.php
<?php require __DIR__ . '/../vendor/autoload.php'; require_once 'PHPUnit/Framework/Assert/Functions.php';
<?xml version="1.0" encoding="utf-8" ?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.github.com/sebastianbergmann/phpunit/master/phpunit.xsd" bootstrap="./bootstrap.php" > <testsuites> <testsuite name="tests"> <directory>./</directory> </testsuite> </testsuites> </phpunit>
SampleTest.php
<?php class SampleTest extends PHPUnit_Framework_TestCase { function test() { $sample = new Sample(); assertEquals(3, $sample->add(1, 2)); } }
composer で phpunit をインストールします。
$ composer install --dev Loading composer repositories with package information Installing dependencies (including require-dev) : Writing lock file Generating autoload files
phpunit を実行します。Sample::add() の中身が未実装なのでテストはコケます。
$ vendor/bin/phpunit -c tests/ --colors PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist F Time: 29 ms, Memory: 3.25Mb There was 1 failure: 1) SampleTest::test Failed asserting that null matches expected 3. /home/ore/work/example/php-grunt/tests/SampleTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
grunt の準備
PHP のソースとテストの準備はできたので grunt を準備していきます。
grunt-cli
まずは npm で grunt-cli をグローバルにインストールします。
理由は知りませんが grunt-cli はグローバルにインストールする必要があります。
(手元で試した感じ -g
をなくしてプロジェクトローカルにインストールしても大丈夫っぽかったですが・・・よく判りません)
$ npm install -g grunt-cli
package.json
続いて package.json を作成します。
$ echo '{}' > package.json
package.json は PHP で言うところの composer.json です。
これにパッケージを書いておけば npm install
だけで必要なパッケージをインストールすることができます。
本来なら npm init
で作成するのですが、PHP の開発のためにちょっと使うだけならこれでも十分です、たぶん。
grunt/grunt-phpunit
次に grunt と grunt-phpunit をインストールします。
先ほどの grunt-cli と異なり -g
を付けていないので、カレントディレクトリの node_modules にインストールされます。
$ npm install --save-dev grunt $ npm install --save-dev grunt-phpunit
これは PHP で言うところの composer require --dev ore/are
です。
パッケージをインストールしつつ package.json にパッケージ名を追記します。
ただ、package.json が自動で作成されたりはしないので、前の手順で空のファイルを作っています。
Gruntfile.js
Gruntfile.js を作成します。このファイルには実行するタスクの内容を記述します。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ phpunit: { options: { bin: 'vendor/bin/phpunit', // phpunit の bin のパス configuration: 'tests/', // --configuration=tests/ colors: true, // --colors followOutput: true // phpunit の出力の都度コンソールに表示する }, test: {} // テスト対象のディレクトリ } }); grunt.loadNpmTasks('grunt-phpunit'); };
この例では次の通りに設定しています。
- phpunit のパスは vendor/bin/phpunit
- オプションは
--configuration=tests/ --colors
- phpunit から出力の都度コンソールに表示する(
followOutput: true
)- followOutput が false だとすべてのテストが終了してから結果が表示されます
- テストの進捗がわからなくなるので followOutput は true にしておいた方が良いでしょう
grunt で phpunit を実行
次のように grunt で phpunit を実行できます。
$ grunt phpunit Running "phpunit:test" (phpunit) task Starting phpunit (target: test) in PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist F Time: 26 ms, Memory: 3.25Mb There was 1 failure: 1) SampleTest::test Failed asserting that null matches expected 3. /home/ore/work/example/php-grunt/tests/SampleTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1. Fatal error: Command failed:
でも、これだけなら普通に phpunit をすればいいですね・・・
なお、test: {}
の部分は普通は次のようにテスト対象のディレクトリを指定します。
test: { dir: "tests/" }
が、この例では phpunit.xml.dist でテスト対象を指定しているので空で構いません。
また、test の部分はタスクのターゲット名になっていて、grunt phpunit:test
のように指定することが出来ます。
なので、次のようにテストを複数に分割して定義することも出来ます。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ phpunit: { options: { bin: 'vendor/bin/phpunit', configuration: 'tests/', colors: true, followOutput: true }, unit: { dir: "tests/unit" }, functional: { dir: "tests/functional" } } }); grunt.loadNpmTasks('grunt-phpunit'); };
$ grunt phpunit:unit # ユニットテスト $ grunt phpunit:functional # ファンクショナルテスト $ grunt phpunit # 両方
grunt phpunit
のように :
の後を省略するとすべてのタスクが実行されます。
最初の例では phpunit のタスクを 1 つしか設けていないので :
の後は省略しています。
grunt でディレクトリ監視して phpunit を実行
次に、grunt でディレクトリを監視してファイルが更新されたら自動的に phpunit が実行されるようにします。
まずはディレクトリ監視のための grunt のプラグインをインストールします。
$ npm install --save-dev grunt-contrib-watch
Gruntfile.js を次のように修正します。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ phpunit: { options: { bin: 'vendor/bin/phpunit', configuration: 'tests/', colors: true, followOutput: true }, test: {} }, watch: { php: { tasks: ['phpunit'], // ファイルの変更時に実行するタスク files: [ 'src/**/*.php', // 監視するパス 'tests/**/*.php' // 同上 ] } } }); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-phpunit'); grunt.registerTask('default', ['watch']); // デフォルトのタスクを watch にする };
grunt watch
でディレクトリ監視を開始します。
$ grunt watch Running "watch" task Waiting...
コンソールの応答がここで止まります。コンソールはそのままで src/Sample.php を修正します。
<?php class Sample { public function func($a, $b) { return $a + $b; } }
自動的に phpunit が実行されてテストが OK になりました。
Waiting...OK >> File "src/Sample.php" changed. Running "phpunit:test" (phpunit) task Starting phpunit (target: test) in PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist . Time: 26 ms, Memory: 3.25Mb OK (1 test, 1 assertion) Done, without errors. Completed in 0.461s at Tue Dec 03 2013 22:43:53 GMT+0900 (JST) - Waiting...
この後も Sample.php や SampleTest.php を編集するたびに自動的にテストが実行されます。
なお、grunt を引数なしで実行すると default というタスクが実行されます。
↑の Gruntfile.js では grunt.registerTask()
で default に設定しているので、grunt
と打つだけで watch タスクが実行されます。
$ grunt Running "watch" task Waiting...
grunt で PHP ビルドインウェブサーバを使う
grunt-php は grunt から PHP のビルドインウェブサーバを実行するためのプラグインです。 ビルドインウェブサーバを実行するにはドキュメントルートが必要なので適当に作っておきます。
public/index.php
<?php require __DIR__ . '/../vendor/autoload.php'; var_dump(date('Y/m/d H:i:s')); $sample = new Sample(); var_dump($sample->add(3, 4));
grunt-php をインストールします。
$ npm install --save-dev grunt-php
Gruntfile.js を次のように書き換えます。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ php: { options: { port: 5000, // リッスンするポート番号 base: 'public' // ドキュメントルート }, dist: {} } }); grunt.loadNpmTasks('grunt-php'); grunt.registerTask('default', ['php:dist']); };
早速実行してみます。
$ grunt Running "php:dist" (php) task PHP 5.5.6 Development Server started at Tue Dec 3 22:44:24 2013 Listening on http://127.0.0.1:5000 Document root is /home/ore/work/example/php-grunt/public Press Ctrl-C to quit. [Tue Dec 3 22:44:24 2013] 127.0.0.1:35661 [200]: / Done, without errors.
一瞬だけビルドインウェブサーバが立ち上がって grunt の終了とともに終了しました。
これだけだとなんの役にも立ちませんが、e2e テストとかで Web サーバが必要なときにテストの実行にあわせてビルドインウェブサーバを立ち上げることが出来ます。
例えば、SampleTest.php を次のように書き換えてみます。
SampleTest.php
<?php class SampleTest extends PHPUnit_Framework_TestCase { function test() { $body = file_get_contents('http://localhost:5000'); assertNotEmpty($body); } }
このまま phpunit を実行しても 5000 ポートで待ち受けている Web サーバが無いのでテストはコケます。
$ vendor/bin/phpunit -c tests/ --colors PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist E Time: 22 ms, Memory: 3.00Mb There was 1 error: 1) SampleTest::test file_get_contents(http://localhost:5000): failed to open stream: Connection refused /home/ore/work/example/php-grunt/tests/SampleTest.php:6 FAILURES! Tests: 1, Assertions: 0, Errors: 1.
そこで Gruntfile.js を次のように書き換えます。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ phpunit: { options: { bin: 'vendor/bin/phpunit', configuration: 'tests/', colors: true, followOutput: true }, test: {} }, php: { options: { port: 5000, base: 'public' }, dist: {} } }); grunt.loadNpmTasks('grunt-php'); grunt.loadNpmTasks('grunt-phpunit'); // タスクを php:dist -> phpunit と続けて実行する grunt.registerTask('default', ['php:dist', 'phpunit']); };
grunt を実行します。
$ grunt Running "php:dist" (php) task PHP 5.5.6 Development Server started at Tue Dec 3 22:46:02 2013 Listening on http://127.0.0.1:5000 Document root is /home/ore/work/example/php-grunt/public Press Ctrl-C to quit. [Tue Dec 3 22:46:02 2013] 127.0.0.1:35667 [200]: / Running "phpunit:test" (phpunit) task Starting phpunit (target: test) in PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist [Tue Dec 3 22:46:02 2013] 127.0.0.1:35669 [200]: / . Time: 30 ms, Memory: 3.25Mb OK (1 test, 1 assertion) Done, without errors.
今度はテストの実行中に grunt がビルドインウェブサーバを立ち上げるので、テストが OK になりました。
grunt-php で単にビルドインウェブサーバを起動する
grunt-php の最初の例では、ビルドインウェブサーバが起動した後、すぐに終了してしまいましたが、 Gruntfile.js で次のように指定すればビルドインウェブサーバを起動したままにすることができます。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ php: { options: { port: 5000, base: 'public' }, web: { options: { keepalive: true, // ビルドインウェブサーバを起動したままにする open: true // タスクの実行時にブラウザを開く } } } }); grunt.loadNpmTasks('grunt-php'); grunt.registerTask('default', ['php:web']); };
grunt を実行すると・・・ビルドインウェブサーバが起動しつつ、ブラウザも起動して http://localhost:5000/ が開かれます。
$ grunt Running "php:web" (php) task PHP 5.5.6 Development Server started at Tue Dec 3 22:51:42 2013 Listening on http://127.0.0.1:5000 Document root is D:\ore\work\example\php-grunt\public Press Ctrl-C to quit. [Tue Dec 3 22:51:43 2013] 127.0.0.1:63381 [200]: / [Tue Dec 3 22:51:43 2013] 127.0.0.1:63382 [200]: /
終了するときは Ctrl-C で終了してください(ブラウザは手で閉じてください)。
でもこれだけだと普通にビルドインウェブサーバを実行すればいいですね・・・
grunt-php でビルドインウェブサーバを起動して LiveReload する
次に grunt でビルドインウェブサーバを立ち上げつつ LiveReload でファイル更新時にブラウザを自動的にリロードするようにします。
LiveReload に関しては このへん で調べればいいと思います。
私は FireFox のアドオンを使ってます。
それでは早速 Gruntfile.js を編集します。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ php: { options: { port: 5000, base: 'public/' }, web: { options: { open: true } } }, watch: { php: { options: { livereload: true // ファイルの更新時に LiveReload する }, tasks: [], files: [ 'public/*.php', 'src/**/*.php' ] } } }); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-php'); // タスクを php:web -> watch と続けて実行する grunt.registerTask('default', ['php:web', 'watch']); };
grunt を実行します。
$ grunt Running "php:web" (php) task PHP 5.5.6 Development Server started at Tue Dec 3 22:59:14 2013 Listening on http://127.0.0.1:5000 Document root is D:\ore\work\example\php-grunt\public Press Ctrl-C to quit. [Tue Dec 3 22:59:14 2013] 127.0.0.1:63404 [200]: / Running "watch" task Waiting...[Tue Dec 3 22:59:14 2013] 127.0.0.1:63405 [200]: /
ブラウザが起動するのでアドオンの LiveReload を有効にします。 FireFox なら な感じのアイコンをクリックして な感じになれば OK です。
そしておもむろに Sample.php を編集します。
Sample.php
<?php class Sample { public function func($a, $b) { return $a * $b; } }
自動的にブラウザがリロードされましたね。
$ grunt Waiting...[Tue Dec 3 22:59:14 2013] 127.0.0.1:63405 [200]: / OK >> File "public\index.php" changed. ... Reload public\index.php ... ... Reload public\index.php ... Completed in 0.000s at Tue Dec 3 2013 22:59:19 GMT+0900 (東京 (標準時)) - Waiting... [Tue Dec 3 22:59:19 2013] 127.0.0.1:63411 [200]: /
Gruntfile.js をまとめる
ここまでの例では簡単化のために phpunit の Gruntfile.js と livereload の Gruntfile.js を別々に作りましたが、1 つのファイルにまとめることも出来ます。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ php: { options: { port: 5000, base: 'public/' }, dist: {}, web: { options: { open: true } } }, phpunit: { options: { bin: 'vendor/bin/phpunit', configuration: 'tests/', colors: true, followOutput: true }, test: {} }, watch: { web: { options: { livereload: true }, tasks: [], files: [ 'public/*.php', 'src/**/*.php' ] }, test: { options: { livereload: false }, tasks: ['phpunit'], files: [ 'src/**/*.php', 'tests/**/*.php' ] } } }); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-php'); grunt.loadNpmTasks('grunt-phpunit'); grunt.registerTask('web', ['php:web', 'watch:web']); grunt.registerTask('test', ['php:dist', 'watch:test']); };
この Gruntfile.js では次の通りにタスクを定義しています。
grunt web
- ビルドインウェブサーバを起動
- ブラウザを開く
- ディレクトリを監視して livereload でブラウザをリロードする
grunt test
- ビルドインウェブサーバを起動
- ディレクトリを監視して phpunit を実行する
grunt-contrib-watch を grunt-este-watch に変更する
grunt-contrib-watch と同じようなプラグインに grunt-este-watch というものがあります。
- steida/grunt-este-watch
- grunt-contrib-watch が重いので grunt-este-watch を試したら幸せになった - てっく煮ブログ
- grunt-este-watch めっちゃ便利やんけ - 音の鳴るブログ
grunt-este-watch の方が CPU 使用率が低くてオススメらしいので、↑で試した phpunit を自動実行する Gruntfile.js を grunt-este-watch を使うように変更してみます。
まずは grunt-este-watch をインストールします。
$ npm install -save-dev grunt-este-watch
Gruntfile.js を書き換えます。説明は省くので↑の記事とかを参考にしてください。
Gruntfile.js
module.exports = function (grunt) { grunt.initConfig({ php: { options: { port: 5000, base: 'public/' }, dist: {} }, phpunit: { options: { bin: 'vendor/bin/phpunit', configuration: 'tests/', colors: true, followOutput: true }, test: {} }, esteWatch: { options: { dirs: [ // 監視対象のディレクトリ // ファイルではなくディレクトリを指定します 'src/**/', 'tests/**/' ], livereload: { // LiveReload を無効にする // すぐしたのコメントと入れ替えれば有効になります enabled: false //enabled: true, //port: 35729, //extensions: ['php', 'js', 'css'] } }, // `php:` の部分で変更されたファイルの拡張子を指定します // 戻り値で実行するタスクを返します // filepath には変更されたファイルのパスが入っています php: function (filepath) { return ['phpunit']; } } }); grunt.loadNpmTasks('grunt-este-watch'); grunt.loadNpmTasks('grunt-phpunit'); grunt.loadNpmTasks('grunt-php'); grunt.registerTask('default', ['php:dist', 'esteWatch']); };
grunt を実行してソースファイルを修正すると自動的にテストが実行されます。
$ grunt Running "php:dist" (php) task PHP 5.5.6 Development Server started at Tue Dec 3 22:48:47 2013 Listening on http://127.0.0.1:5000 Document root is /home/ore/work/example/php-grunt/public Press Ctrl-C to quit. [Tue Dec 3 22:48:47 2013] 127.0.0.1:35682 [200]: / Running "esteWatch" task >> Waiting... 4 dirs watched within 4 ms. >> User action. >> File changed: src/Sample.php Running "phpunit:test" (phpunit) task Starting phpunit (target: test) in PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read from /home/ore/work/example/php-grunt/tests/phpunit.xml.dist [Tue Dec 3 22:48:49 2013] 127.0.0.1:35684 [200]: / . Time: 27 ms, Memory: 3.00Mb OK (1 test, 1 assertion) Running "esteWatch" task >> Waiting...
なお、grunt-este-watch はディレクトリ監視を Linux なら inotify、Windows なら ReadDirectoryChangesW で実装されているため(正しくは grunt-este-watch が使っている fs.watch が)、 ネットワークファイルシステムなどで動作しない場合があります(Linux の cifs では動作しませんでした)。
さいごに
他にも定番ものだと grunt-phpcpd や grunt-phpmd などもあるので、 CPD や PMD をやってる人は使ってみるといいのではないでしょうか。