sometimes I laugh

高専卒WEBエンジニアがいろいろ残しておくブログ

JenkinsでFuelPHPのユニットテストを自動化してみた

f:id:watass:20150111232633p:plain

Jenkins、いいですよね。オペレーションの自動化には美があります。前回の記事ではGitによるデプロイ自動化を行いましたが、その時点で既にユニットテストの自動化は想定していたので、今回はそれを実現してみます。
これによって、機能を実装してプッシュする度に自動でデプロイされ、その過程でテストが自動で行われるので、特に通知がなければ動作が保証されるようになります。実際に本番環境への適用の際には、もう少ししっかりテストをする必要があるのかもしれませんが、開発環境レベルではテストを意識的に実施せずとも気兼ねなく開発を進めることができます。
ユニットテストにはおなじみのPHPUnitを使います。なお、前回のJenkins導入やデプロイ自動化の話はこちらの記事でまとめてありますのでよろしければどうぞ。


ユニットテストとテスト自動化

従来、プログラムのテストはミスの許されない商用プロダクトでは欠かせない作業ですが、面倒臭いですし、不毛な作業になりかねません。そこで、過去の偉いプログラマはテストをコード化し、面倒なテストという作業をプログラミングで解決することにしました。
もちろん、テストがプログラムになることによって、そのテスト自体にバグが発生する可能性は否めないのですが、プログラムになることでテストの自動化ができ、任意のタイミングで好きなだけテストが実行できるようになりました。私のような心配性には最高です。

とはいえ、実際にテストをする際には、いちいち変数を比較するif文が必要になったり、データベースが絡む場合には色々面倒だったりします。そんなテストにおける煩雑さを解消するために、PHPではPHPUnitというテスティングフレームワークが存在します。
FuelPHPのコアクラスもPHPUnitによるテストを前提にして実装されているため、FuelPHPとも相性が非常にいいです。では早速、作成済みのFuelPHPアプリケーションにテストプログラムを実装し、Jenkinsによる自動テストを試してみましょう。以前に作成したFuelPHPアプリケーションについては、以下の記事をご覧ください。


開発環境について

サーバ構成はGitによる自動デプロイのときと同じ構成ですので、主に使用するサーバはJenkinsサーバと本番サーバになります。使用しているアプリケーションのバージョンは以下の通りです。

・Jenkinsサーバ
OS : Amazon Linux AMI 2014.09
Java : version 1.7.0_71
Jenkins : version 1.596

・本番サーバ
OS : Amazon Linux AMI 2014.09
FuelPHP:1.7.2
PHPUnit:3.7.38


PHPUnitの導入

FuelPHPではcomposerを用いてPHPUnitをインストールすることが推奨されていますので、本番サーバにログインして、まずはcomposer.jsonを以下の通りに編集しましょう。

// 前略
    "minimum-stability": "stable",
    "require-dev": {
        "phpunit/phpunit": "3.7.*"
    }
// 後略

minimum-stabilityはFuelPHPのインストール時には"dev"になっていますが、"stable"にすることが望ましい、とのことだったので、ついでに修正しておきました。PHPUnitに関する記述はrequire-devで、phpunitの3.7系がインストールされるように設定してあります。
composer.jsonを編集したら、プロジェクト配下へ移動し、以下のコマンドでPHPUnitをインストールします。

$ cd /home/ec2-user/welcome
$ php composer.phar update
$ php composer.phar require phpunit/phpunit:3.7.* --dev

これでインストールが完了すればOKです。ついでに実行するためにはPHPの各種パッケージが必要な場合があるため、インストールしていない場合にはインストールしておきましょう。

# yum install php-xml php-mbstring

インストールができたら、バージョン確認してみましょう。

$ fuel/vendor/bin/phpunit --version
PHPUnit 3.7.38 by Sebastian Bergmann.

いいですね。無事インストールできました。


コアクラスのテスト実行

PHPUnitのインストールはできましたが、コマンドがちょっと面倒臭いですね。FuelPHPのoilコマンドを使うことで、テストがより簡単になりますので、まずはoilコマンドでPHPUnitを実行できるように設定します。fuel/package/oil/config/oil.phpファイルをfuel/app/config配下にコピーして、それを以下のように編集します。

// 前略
                'binary_path' => VENDORPATH.'bin/phpunit' ,
// 後略

上記のように、PHPUnitのパスを通すことで、oil testコマンドでPHPUnitが実行できるようになります。FuelPHPでは、コアクラスにテストコードが実装されているので、そのテストを以下のコマンドで実行してみましょう。

$ php oil test --group=Core
Tests Running...This may take a few moments.
PHPUnit 3.7.38 by Sebastian Bergmann.

Configuration read from /home/ec2-user/welcome/fuel/core/phpunit.xml

...............................................................  63 / 375 ( 16%)
............................................................... 126 / 375 ( 33%)
............................................................... 189 / 375 ( 50%)
............................................................... 252 / 375 ( 67%)
............................................................... 315 / 375 ( 84%)
............................................................

Time: 211 ms, Memory: 25.25Mb

OK (375 tests, 447 assertions)

コアクラスのテストはアノテーションとしてCoreが付属されているので、groupにてCoreを指定することでコアクラスのテストが実行可能です。正常にインストールされていれば、上記のようにすべてOKで完了します。
実行してみればわかりますが、実に375ものテスト、447項目もの比較がたった一瞬で完了したことがわかります。この強力さこそ、テスト自動化のメリットですね。


コントローラーのテストコードを作成

コアクラスの正しさを毎回テストできるだけでも確かに嬉しいのですが、自作のクラスなどがテストできることがやはり理想です。ここからが本番です。以前作成した、以下のようなlogin.phpのコントローラクラスのテストコードを検討します。

<?php

class Controller_Login extends Controller{
        public function action_index($id = 1){
                $user = Model_Post::find_one_by('id',$id);
		$data['name'] = $user->name;

                return View::forge('login_view',$data);
        }
}

データベースに接続して、IDに紐づくユーザ名を取り出し、それをViewで表示するだけ、という非常にシンプルなクラスですが、データベースが絡む上に、内部の変数を取り出す機構がないため、テストのコーディングをあまりやったことがない私みたいなタイプでは、ちょっとむずかしいです。
FuelPHPではコントローラのテストはfuel/app/tests/controller配下に作成することができます。名前はテストするクラス名になりますので、login.phpとします。内容は以下の通りです。

<?php

/**
* Login class Tests
* @group App
*/
class Test_login extends TestCase{
	public function test_IDが1ならユーザ名はdbuserになる(){
		$expected = 'dbuser';
		$response = Request::forge('login/index')
								->execute()
								->response();
		$this->assertSame($expected,$response->body->name);
	}
}

きっとテストのコードに慣れていないと、ぎょっとするプログラムだと思います。なんせメソッド名が日本語ですからね。しかし、テスト内容はわかりにくくなりがちですし、下手な英語で書くと、テスト失敗時に原因を探すのが地味に面倒です。何より、メソッド名に日本語を採用しても特に問題はないそうです。
ちなみにこのテストプログラムはTestCaseクラスを継承し、実際にビューの表示までリクエストしてみて、その結果の変数を予想と比較しよう、というものです。@groupアノテーションでは、現在は仮に"App"とつけていますが、曖昧すぎるので、実際にはもう少しジャンル分けは意識したほうがよさそうです。
作成したテストプログラムはGitHubにプッシュし、自動でデプロイさせましょう。


作成したテストをPHPUnitで実行

テストコードの本番環境へのデプロイが完了したら、本番サーバへログインし、oil testコマンドで早速自作テストを手動で実行してみましょう。groupアノテーションをAppにして以下のように実行します。

$ php oil test --group=App
Tests Running...This may take a few moments.
PHPUnit 3.7.38 by Sebastian Bergmann.

Configuration read from /home/ec2-user/welcome/fuel/core/phpunit.xml

.

Time: 26 ms, Memory: 21.00Mb

OK (1 test, 1 assertion)

これでOK。データベースの参照が(少なくともid=1に関しては)問題ないことが確認できました。この調子でどんどんテストを実装することで、確認する項目を増やすことができます。確認項目を増やしても、よほどの数にならない限り、テストの関するコストはほとんど変わりません。


JenkinsでGitデプロイ時にPHPUnitを実行

以上でテストがコマンドベースでできるようになったので、Jenkinsで自動化するだけです。シェルでかけるということは、Jenkinsのジョブ設定で、デプロイのついでにoil testを実行するだけですから、設定には苦労しません。こんな感じです。

ssh -i /var/lib/jenkins/.ssh/[鍵名] ec2-user@255.255.255.255 "cd /home/ec2-user/welcome/ ; git pull origin develop ; php oil test --group=Core ; php oil test --group=App"

これでGitHubにプッシュされるたびに自動でデプロイされ、ついでにコアクラスと自作クラスの確認をやってくれます。失敗したらJenkinsさんが教えてくれるので素晴らしいですね。
適用に修正してGitHubにプッシュしてみました。Jenkinsでジョブが走っているのがわかりますね。完了したところで、コンソール出力を見てみましょう。

Started by GitHub push by wata727
Building in workspace /var/lib/jenkins/workspace/develop-branch-auto-deploy
 > git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url git@github.com:wata727/welcome.git # timeout=10
Fetching upstream changes from git@github.com:wata727/welcome.git
 > git --version # timeout=10
 > git -c core.askpass=true fetch --tags --progress git@github.com:wata727/welcome.git +refs/heads/*:refs/remotes/origin/*
 > git rev-parse refs/remotes/origin/develop^{commit} # timeout=10
 > git rev-parse refs/remotes/origin/origin/develop^{commit} # timeout=10
Checking out Revision 7fdc814b1466477789036eb3dda8976037f24d9e (refs/remotes/origin/develop)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 7fdc814b1466477789036eb3dda8976037f24d9e
 > git rev-list 0719110240266948b9d306b6526e8c5d57e556ea # timeout=10
[develop-branch-auto-deploy] $ /bin/sh -xe /tmp/hudsonXXXXX.sh
+ ssh -i /var/lib/jenkins/.ssh/[鍵名] ec2-user@255.255.255.255 'cd /home/ec2-user/welcome/ ; git pull origin develop ; php oil test --group=Core ; php oil test --group=App'
From https://github.com/wata727/welcome
 * branch            develop    -> FETCH_HEAD
   0719110..7fdc814  develop    -> origin/develop
Updating 0719110..7fdc814
Fast-forward
 fuel/app/views/login_view.php | 2 ++
 1 file changed, 2 insertions(+)
[0;32mTests Running...This may take a few moments.[0m
PHPUnit 3.7.38 by Sebastian Bergmann.

Configuration read from /home/ec2-user/welcome/fuel/core/phpunit.xml

...............................................................  63 / 376 ( 16%)
............................................................... 126 / 376 ( 33%)
............................................................... 189 / 376 ( 50%)
............................................................... 252 / 376 ( 67%)
............................................................... 315 / 376 ( 83%)
............................................................

Time: 200 ms, Memory: 25.25Mb

[30;42m[2KOK (375 tests, 447 assertions)
[0m[2K[0;32mTests Running...This may take a few moments.[0m
PHPUnit 3.7.38 by Sebastian Bergmann.

Configuration read from /home/ec2-user/welcome/fuel/core/phpunit.xml

.

Time: 24 ms, Memory: 21.00Mb

[30;42m[2KOK (1 test, 1 assertion)
[0m[2KFinished: SUCCESS

素晴らしいですね!これでいちいちやっていたテストやデプロイの手間から脱出できます。Jenkinsさんを導入したことによって、GitHubにプッシュするだけで全部Jenkinsさんがやってくれるので大変助かりますね。


まとめ

  • FuelPHPユニットテストはoil testコマンドでPHPUnitが実行可能
  • fuel/app/tests配下にテストプログラムはまとめられる
  • コマンドベースでテスト可能なため、Jenkinsのジョブでシェル実行可能


テストコードを書くには経験が必要ですね

今回、初めてテスティングフレームワークを用いてユニットテストのコードを実装したのですが、なかなか特徴的な記述も多く、慣れるためにはもう少し色々なパターンのテストを経験するしかないんですね。
とはいえ、テストは決して万能ではありませんので、実装時には様々な工夫が必要になるのかなと思います。例えば、コードカバレッジをレポートする機能などもありますので、なるべくテストそのものの妥当性を検証する仕組みは考えなくてはいけませんね。自動化されたテストは、あくまでも簡易的なものとして、本番への適用は厳密な総合テストを実施するようにしたほうがいいかもしれません。

なお、今回のPHPUnitの導入では本番環境を編集してしまったわけですが、それによってブランチ別にGitHubにプッシュしないといけなくて、クライアント側もブランチ別にプルしないといけなくて〜のようにだいぶ面倒臭かったです。できれば設定変更や新規ツール導入もクライアントサイドでやりたいですよねー。
サーバ設定変更用にトピックブランチとしてコアブランチみたいなものを用意してもいいかもしれないです。後、クライアントサイドで動作確認できるよう、Vagrantなどの導入も検討したいですね。


参考にした記事

はじめてのフレームワークとしてのFuelPHP

はじめてのフレームワークとしてのFuelPHP 改訂版

はじめてのフレームワークとしてのFuelPHP 改訂版

# 大変参考になりました。ユニットテストについては完全初心者だったのでわかりやすかったです。
composerでPHPUnitがインストールできない - 終電23時15分って早くね?
# 同様の現象で詰んでいたので大変助かりました。Composerもう少し詳しくなりたいですね。
FuelPHPでユニットテストを実行すると mb_strlenという関数が未定義とエラーになる(1061)|teratail
# 同様の現象でハマってました。まぁPHP関連の基本パッケージは入れとけって話ですよね。
FuelPHP【チュートリアル】-- コントローラーのテストの方法 -- - Qiita
# 具体的なFuelPHPユニットテスト方法についてイメージできてよかったです。
FuelPHPのコントローラのテストは割と簡単? - サンゴラボ
# FuelPHPのコントローラのテストコードとして一番参考になりました。ありがとうございます。