雑文発散

«前の日記(2014-02-01) 最新 次の日記(2014-02-03)» 編集
過去の日記

2014-02-02 [長年日記]

[Grunt][PHPUnit] PHPUnit のテストファイルを Grunt で監視して「変更があったファイルのテスト自動実行」ができるようになるまでの記録

今回、やりたいことは、ちょっと前の日記にも書いたけど、こんな内容のことを実施したいと思っている。

  • PHPUnit のテストファイルを修正したら、修正したファイルだけを対象としてテストを実行自動したい。

今更この程度のことかよ!?という内容かも知れないけど、この程度のことができないのも事実なので、淡々と学んでいくしかない。

現在は手動で次のようなコマンドを入力(もちろん履歴とか使うけど)して PHPUnit のテストを実行している。

% phpunit -c phpunit.xml [対象のテストファイル]

phpunit.xml は、コンフィグファイルであり、普段使う設定が書き込まれている。その設定では、プロジェクト内の全てのテストを実行が指定されている。必ずしも -c オプションでコンフィグを指定する必要はないのだけど、複数の設定ファイルを使い分けている関係で常用している。

ひとつのテストメソッドをちょっとして修正の動作確認をしたい場合には、全テストはちょっと大げさかつ効率が悪い。ファイル名を引数として渡すことで「いま修正しているファイル」を対象にテスト実行をしている。

更に実行時間を短くしたい場合には、--filter オプションに修正中のテストメソッド名を指定して、実行対象メソッドも限定しているのだが、今回の「ファイル変更の監視」だと、修正中のメソッドまでは把握できなさそうなので、いったん置いておく。


この記事用に作ったサンプル用の PHPUnit 環境はこんな感じにしている。

% ls -1
composer.json
composer.lock
composer.phar*
phpunit.xml
tests/
vendor/

PHPUnit は Composer を使ってインストール。composer.json の中身はこれだけ。

% cat composer.json
{
    "require": {
        "phpunit/phpunit": "3.7.*"
    }
}

テストファイルはこんな風にしてみた。

% ls -1
fugaTest.php
hogeTest.php

それぞれのテスト内容は、ダミーなので、とりあえず絶対成功するヤツを書いている。

% cat tests/fugaTest.php
<?php

class FugaTest extends PHPUnit_Framework_TestCase
{
    public function testDummy1()
    {
        $this->assertFalse(false);
    }
}
% cat tests/hogeTest.php
<?php

class HogeTest extends PHPUnit_Framework_TestCase
{
    public function testDummy2()
    {
        $this->assertTrue(true);
    }
}

これを実行すると、「OK」になる。これで全てのテストが成功している状態。

% ./vendor/bin/phpunit -c phpunit.xml
PHPUnit 3.7.29 by Sebastian Bergmann.

Configuration read from /Users/suzuki/work/grunt/phpunit.xml

..

Time: 36 ms, Memory: 2.50Mb

OK (2 tests, 2 assertions)

さて、ここから本題。

まず npm init を使って、package.json ファイルを作成してみた。

% npm init

いろいろと質問されてくるので、適当に答えつつ出来上がった package.json の中身はこんな感じ。いろいろ適当なのだけど、とりあえずローカルで使うだけなので、まぁ、いいか、と。

{
  "name": "phpunit-watch",
  "version": "0.0.0",
  "description": "PHPUnit test watch",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "BSD-2-Clause"
}

次に Grunt 関連のインストール。grunt-cligrunt-contrib-watchgrunt-phpunit の3つのパッケージを指定してみる。grunt-contrib-watchgrunt-phpunit は npm のパッケージであると同時に、Grunt のプラグインという位置付けでもある。

% npm install grunt-cli grunt-contrib-watch grunt-phpunit --save-dev

--save-dev オプションを付けると、インストールしたパッケージを package.json へ記録してくれるそうだ。

{
  "name": "phpunit-watch",
  "version": "0.0.0",
  "description": "PHPUnit test watch",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "BSD-2-Clause",
  "devDependencies": {
    "grunt-phpunit": "~0.3.2",
    "grunt-cli": "~0.1.13",
    "grunt": "~0.4.2",
    "grunt-contrib-watch": "~0.5.3"
  }
}

確かに devDependencies に追記されている。

また、install したパッケージは、node_modules ディレクトリ以下にインストールされているのが分かる。

% ls -1
composer.json
composer.lock
composer.phar*
node_modules/
package.json
phpunit.xml
tests/
vendor/

node_modules

% ls -1 node_modules/
grunt/
grunt-cli/
grunt-contrib-watch/
grunt-phpunit/

必要なパッケージのインストールが終わったら、今後は実行用の設定に入る。設定は Gruntfile という名前のファイルに記述する。JavaScript 版の Gruntfile.js と CoffeeScript 版の Gruntfile.coffee が利用できる。

grunt コマンドの設定ファイル Gruntfile は、make コマンドに対する Makefile のような存在だと思えばピッタリくる模様。

まずは、grunt コマンドで PHPUnit の実行ができるように設定を書く。今回は、JavaScript 版の Gruntfile.js にした。

module.exports = function(grunt) {

  grunt.initConfig({
    phpunit: {
      classes: {
        dir: './tests/'
      },
      options: {
        bin: './vendor/bin/phpunit',
        configuration: './phpunit.xml'
      }
    }
  });

  // パッケージの読み込み
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-phpunit');
}

phpunit: 以下に書いてある設定が grunt-phpunit のもの。ここでは、テスト対象のクラスを指定している classes:options: を利用している。

classes:dir: でテスト対象のディレクトリを指定するようだ。phpunit.xml の中でもテスト対象を指定しているのでダブってしまうのだけど、ここはそういうものとして利用する。

options: の内容は、PHPUnit のコマンドラインオプションと対比してあるようなので、マニュアルとにらめっこすれば、意味が分かると思う。

この状態で grunt コマンドにタスク名 phpunit を指定して実行すると、PHPUnit が走って、先ほどの結果と同じ出力を得られる。

% ./node_modules/.bin/grunt phpunit
Running "phpunit:classes" (phpunit) task
Starting phpunit (target: classes) in tests/
PHPUnit 3.7.29 by Sebastian Bergmann.

Configuration read from /Users/suzuki/work/grunt/phpunit.xml

..

Time: 18 ms, Memory: 2.50Mb

OK (2 tests, 2 assertions)

Done, without errors.

今度は最終目的である「ファイルの変更を監視」をする設定を書く。これは grunt-contrib-watch で実現できた。

module.exports = function(grunt) {

  grunt.initConfig({
    phpunit: {
      classes: {
        dir: './tests/'
      },
      options: {
        bin: './vendor/bin/phpunit',
        configuration: './phpunit.xml'
      }
    },
    watch: {
      scripts: {
        // 監視対象のファイル
        files: ['./tests/*Test.php'],
        // 変更を検知した時に実行するタスク
        tasks: ['phpunit'],
        options: {
          spawn: false,
        }
      }
    }
  });

  // watch イベントが発生したら、phpunit: classes: dir: の値を動的に変更
  grunt.event.on('watch', function(action, filepath, target) {
    grunt.config(['phpunit', 'classes', 'dir'], filepath);
  });

  // パッケージの読み込み
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-phpunit');
}

追記したのは、watch: { ... } のブロックと grunt.event.on('watch', ... }); のブロック。

watch: { ... } 内で、監視対象のファイルと、変更のあった場合に実行するタスクに phpunit タスクを指定している。これだけの指定でも、「監視対象のファイルが更新されたら phpunit タスクを実行する」ということなら実現できる。

でも、いまやりたいのは、「修正(更新)したファイルだけを対象にしたい」ということなので、もうひと工夫しているのが、grunt.event.on('watch', ... }); のブロック。

上記のように書くと、watch イベントが発生した場合に、phpunit タスクの classes:dir: を設定しなおしてくれるようだ。つまり、テスト対象のディレクトリ(ファイル)の設定を「変更のあったファイル(filepath)」としてくれるのだ。

その動的な設定変更の後で phpunit タスクが実行されるので、目的であった「修正(変更)があったテストファイルのみテスト実行する」がこれで実現できる。


では、実行してみよう。

監視するには、引数にタスク名 watch を付けて grunt を起動する。

% ./node_modules/.bin/grunt watch
Running "watch" task
Waiting...

この状態で、エディタでテストケースを修正すると、その変更を検知して、phpunit タスクを実行してくれる。

例えば、fugaTest.php ファイルを更新してみよう。

<?php

class FugaTest extends PHPUnit_Framework_TestCase
{
    public function testDummy1()
    {
        $this->assertFalse(false);
        $this->assertTrue(true); // このアサーションを追加
    }
}

すると、先ほど watch 状態になっていたコンソールには、このような出力が出た。

% ./node_modules/.bin/grunt watch
Running "watch" task
Waiting...OK
>> File "tests/fugaTest.php" changed.


Running "phpunit:classes" (phpunit) task
Starting phpunit (target: classes) in tests/fugaTest.php
PHPUnit 3.7.29 by Sebastian Bergmann.

Configuration read from /Users/suzuki/work/grunt/phpunit.xml

.

Time: 16 ms, Memory: 2.50Mb

OK (1 test, 2 assertions)

Running "watch" task
Completed in 0.108s at Sun Feb 02 2014 16: 18 :07 GMT+0900 (JST) - Waiting...

ファイルの変更が検知され、

>> File "tests/fugaTest.php" changed.

そのファイルを対象に実行されている様子が見える。

Starting phpunit (target: classes) in tests/fugaTest.php

実際に使う場合のシナリオは、ひとつのコンソールで grunt watch させた状態で置いておき、その隣でエディタを使ってテストケースを編集・保存、テストの結果を見つつ、更に修正という形になるかな。


今回の Grunt 関連のまとめ。

  • PHPUnit 用に grunt-phpunit プラグインが存在する。
    • phpunit コマンドのオプションがいろいろ使える。
  • ファイルの変更を検知し、タスクを実行するには grunt-contrib-watch プラグインを使えば良い。
    • タスク実行時に、動的に設定を書き換えることができる。

なお、Grunt のプラグインには、シェルコマンドを実行するgrunt-shell もあったので、こちらを使えば専用プラグインが用意されていない場合でも好きなことができそうだ。

これで Grunt の使い方の第一歩は把握できたので、仕事なりその他なりで活用していきたいと思う。

【追記】

「ファイルを監視してタスクを実行する」なら Guardwatchr でもいいんじゃないか?的なコメント(?)を貰った。

今回は「Grunt 勉強しなきゃ → おや、watch なんてできるんだ → おや、phpunit のプラグインもあるんだ」という流れがあったので、Grunt 以外の実現方法を調べようという考えには至らず、それらの存在に気がついていなかったという。。。

watchr とかの記事をググると、2011年ころの記事が多くヒットしたりして、自分の技術とか知識とかがいかに遅れているかという実感を得ちゃうねぇ。。。がんばろ。