ちゃんとテストしたい
PHPでプログラミングしている時、バグに悩まされることが多いです…。
そこで、いろいろ勉強しているうちにユニットテストという方法があることをしりました。
よりバグの少ない堅牢なプログラムを効率よく作るには必要な手段らしいです。テストを行うのは最近では当たり前になっているそうですよ。
@MINOもぜひ恩恵に預かりたい。
よくわからないなりにも頑張ってテスト環境を構築しましたので、その成果を記事に。
テスト環境構築の構想
目標にする項目
今回のチャレンジとして
- Dockerで構築
- データベーステストもできるようDBとの連携を行う
- gitのhook機能を使って
commit
時に自動的にテストが開始されるようにする。
を目標にしました。テストは重要ですが、テストコードを書く必要があることなど工数が余計に掛かる面があることは否めません。
できるだけ作業時間を短縮するためテストは自動化するのが吉とのことです。
今回はgit commit
をトリガーにして自動的にテストを行う方法にチャレンジします。
今回のマシン(Dockerホスト)は以下です。@MINOのASUSノートですよ。
- OS GNU/LINUX Debian8 jessie
- メモリ 6GB
- HDD 250GB
今回の記事では、それほど環境差がでないと思いマス(たぶん)。
処理の要や機能などについて
前置きが長くなりますが、今回の要になっている部分を説明したいと思います。
処理の引き金になるgit commit
gitはおそらくみなさん活用されていることでしょう。@MINOは最近になってやっとなんとか使えるレベルになりました。今回はこのgitでコードの管理をしたいと思っています。
gitは便利なことに、hook機能をもっています。これはあるgit
コマンドを実行した時に、その実行の前後に別な処理を挟むことができる機能です。hookは引っ掛けるフックの意味です(多分)。
hookにはいくつかの種類がありますが、今回はcommit
を実行した時に発動するpre-commit
を使いたいと思います。
このpre-commit
はcommit
メッセージが入力される前、つまりコミット前に実行されます。
hook機能の仕組み・hooksディレクトリの中身
gitリポジトリを作ると、.git
ディレクトリの中にいくつかのディレクトリが作られます。そのなかにhooks
というディレクトリがあるかと思います。
hooks
ディレクトリの中にさらにいくつかのファイルがあります。
- pre-commit.sample
- pre-push.sample
- update.sample
- pre-applypatch.sample
- 等
gitのバージョンや環境によって異なるかもしれませんが、おそらく空ということは無いかと思います。
拡張子がsampleとなっていることからお分かりのとおり、これらはhook機能のサンプルです。
これらのファイルの中身はスクリプトなんですね。(シェルスクリプトの場合が多いようですが、シェルスクリプトには限りません)
ファイル名がhookタイミングを表している
ファイル名がhookのタイミングを表しています。
例えば
pre-commit
コミット前pre-push
プッシュ前
といった意味になります。
拡張子のsampleを外すとgitが勝手に解釈してくれて、然るべきタイミングでスクリプトを実行してくれます。
commit
の前のタイミングでスクリプトを実行したいなら、ファイル名が拡張子なしのpre-commit
というファイルを用意すれば動いてくれます。便利デス。
後述する実作業の説明で詳しく説明します。
Dockerの構成
今回はDockerを使ってテスト環境を整えます。Dockerを使うことで異なるバージョンのPHPでテストできたり、構成を変えたりが容易にできます。
VitualBox
などでテストに使う仮想マシンを立てるのもいいのですが、本来目的ではないOSの設定などを行わなければならず面倒な点もあります。
VitualBox
などの完全仮想化とDockerなどのコンテナ仮想のどっちがいいかということは@MINOには判断がつきませぬので説明できません…。
テストに限るのであれば、すくなくとも準備が楽であることと、環境のカスタマイズが容易という点でDockerがいいような気がしています。
今回は以下のimage
・Dockerfile
を使います。
- phpunit/phpunit
- mysql(公式)
テストの処理の流れ
まとめるとこんな感じで処理が進むことをイメージしています。
- テストコードを書く
- テスト対象コード書く
commit
するpre-commit hook
を起動させるpre-commit hook
で指定されたスクリプトを実行- スクリプトによりDockerのコンテナが立ち上がり
phpunit
がphpunit.xml
によって実行される - テスト成功したら
commit
実行へ - テスト失敗したら
commit
中止へ(コードの修正へ)
これを繰り返してコーディングしていこうということデス。
テスト環境の構築
Dockerの導入
Dockerの導入は事前に行ってください。以下は公式のドキュメントです。
Docker公式のインストールガイド(英語)
左側のメニュー→Docker Engine →install→各OSの説明 Mac、WindowはもちろんLinuxは主要ディストリビューションのほとんどのインストール方法解説を網羅しています。
また拙ブログでもインストールに関して記事にしていますので参照してください。
phpunit/phpunitのカスタマイズ
ここではPHPのテストワークフレームであるPHPUnit
とMySQL
とを連携する際に必要になるいくつかの設定を追加していきます。その際Dockerfile
を編集することになります。
Dockerfile
はimage
をビルドする為の設計図みたいなものです。
コンテナの中に入って必要になるツールなどをインストールする手もあるのですが、作業としては意外と面倒です。また手動になってしまうので、再現性に欠けます。
環境構築をDockerfile
に記述することで、環境の再現性・可搬性が持てます。そして何より楽です。
まずはDocker
上のPHPUnit
イメージであるphpunit/phpunit
のGitHubリポジトリからDockerfile
をゲットしましょう。このphpunit/phpunit
はPHPUnit
開発者謹製です。
phpunit/phpunit
のGitHubリポジトリは以下です。今回は(記事執筆時2016/2現在)最新バージョンである5.1.0を使いたいと思います。
任意のディレクトリにclone
してゲットします。
$ git clone https://github.com/JulienBreux/phpunit-docker.git
phpunit/phpunitの中身
phpunit/phpunit
のDockerfile
を見て行きましょう。
phpunit/phpunit
はcomposer/composer
というimage
がベースになっています。名前からわかる通りPHPのパッケージ管理ソフトのComposerです。
composer/composer
自体は公式のPHP
が元になっています。PHP
はさらにdebian:jessie
が元になっています。
つまりphpunit/phpunit
はPHP
とComposer
とPHPUnit
をインストールしたDebian8
であるということです。
実際にDockerfile
上でPHPunit
のインストール記述があります。Dockerfile
の23行目あたりから以下のような記述があると思います。
# Run composer and phpunit installation.
RUN composer selfupdate &&
composer require "phpunit/phpunit:~5.1.0" --prefer-source --no-interaction &&
ln -s /tmp/vendor/bin/phpunit /usr/local/bin/phpunit
RUN
というコマンドはDockerfile
のコマンドで「以下のコマンドを実行せよ」というものです。 この部分では
コマンド | 意味 |
---|---|
composer selfupdate | Composerのセルフアップデート |
composer require "phpunit/phpunit:~5.1.0" –prefer-source –no-interaction | phpunit5.1.0のインストール |
ln -s /tmp/vendor/bin/phpunit /usr/local/bin/phpunit | /usr/local/bin/phpunitへのシンボリックリンクの作成 |
を行っています。インストール手順がしっかり記述されていますよね。
PHPエクステンションの確認
phpunit/phpunit
のベースになっているcomposer/composer
のDockerfile
にはmcrypt、 zip、 bz2、 mbstring、gd
エクステンションが最初からインストールされるようDockerfile
に記述されていマス。
# PHP Extensions
RUN docker-php-ext-install mcrypt zip bz2 mbstring
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/
&& docker-php-ext-install gd
またphpunit/phpunit
のDockerfile
ではxdebug、pcntl
エクステンションがインストールされるよう記述されていマス。
# Run xdebug installation.
RUN curl -L http://pecl.php.net/get/xdebug-2.3.3.tgz >> /usr/src/php/ext/xdebug.tgz &&
tar -xf /usr/src/php/ext/xdebug.tgz -C /usr/src/php/ext/ &&
rm /usr/src/php/ext/xdebug.tgz &&
docker-php-ext-install xdebug-2.3.3 &&
docker-php-ext-install pcntl &&
php -m
PDOインストール
しかしながらMySQLにつなぐ際に必要なPDO
エクステンションを入れないと後述するDBUnit
が使えません(はず)。
なのでPDO
エクステンションを追加するようDockerfile
に記述を追加します。
エクステンションをインストールしている記述はphpunit/phpunit
の12行目あたりにあります。 そこに以下のような記述を足して、PDO
エクステンション(pdo_mysql
)をインストールするようにします。
# Run xdebug installation.
RUN curl -L http://pecl.php.net/get/xdebug-2.3.3.tgz >> /usr/src/php/ext/xdebug.tgz &&
tar -xf /usr/src/php/ext/xdebug.tgz -C /usr/src/php/ext/ &&
rm /usr/src/php/ext/xdebug.tgz &&
docker-php-ext-install xdebug-2.3.3 &&
docker-php-ext-install pcntl &&
docker-php-ext-install pdo_mysql && //←ここ
php -m
他にも必要なエクステンションがあればここの記述に足していくことでインストールしていくことができます。
DBUnitのインストール
phpunit/phpunit
はデータベース関連のテストを楽にするDBUnit
が入っていません。DBUnit
を使わなければ問題ないのですが、少なからずデータベース周りのテストをしなければならない場合も出てくると思います。
そこでDBUnit
も追加しておきましょう。
ここにDBUnit
のインストール記述を足しマス。 以下のようにしました。
# Run composer and phpunit installation.
RUN composer selfupdate &&
composer require "phpunit/phpunit:~5.1.0" --prefer-source --no-interaction &&
ln -s /tmp/vendor/bin/phpunit /usr/local/bin/phpunit &&
composer require "phpunit/dbunit:~2.0.2" --prefer-source --no-interaction
この記事を書いている時のphpunit/dbunit
(composerのパッケージにおいての)最新バージョンは(記事執筆時2016/2現在)2.0.2だったのでそれをインストールします。
コマンド | 意味 |
---|---|
composer require "phpunit/dbunit:~2.0.2" –prefer-source –no-interaction | phpunit/DBUnit2.0.2のインストール |
インストールに用いている--prefer-source
オプションは「推奨(必ずしも必要ではない)になっている依存関係パッケージをインストールしない」という意味で、 --no-interaction
は「対話形式進行にしない」という意味です。
イメージのビルド
Dockerfile
からimage
を作成することをbuild
(ビルド)と言います。
今まで編集してきたDockerfile
からbuild
したimage
はpdo_mysql
とDBUnit
がインストールされた状態になります。
build
は以下のように行います
$ docker build -t hoge/hoge:1.0 /DokerfilePath
コマンド | 意味 |
---|---|
docker build | 指定されたDockerfileを元にimageをbuild |
-t hoge/hoge | buildされるimageの名前。ベンダー名/image名にするのが推奨されている |
:foo | buildされるimageのタグ名。通常バージョン等の整理に使われる |
/DokerfilePath | Dockerfileがあるディレクトリのパス。Dockerfileという名前のファイルを自動認識するので、カレントディレクトリにDockerfileがあるなら指定不要 |
ベンダー名は自分の名前とかgithubなんかのidとかでいいのかな?
@MINOは以下のようにしました。
$ docker build -t mino/phpunit:1.0
タグ名は必須では無いので必ずしも必要ないですが、image
のバージョン管理を行いたい場合はキッチり設定した方が良いと思いマス。
コンテナ起動時の挙動
各image
でコンテナ起動時の挙動が異なります。作業ではないのですが、その点について少し説明を。
ボーリュームの設定とマウント
phpunit/phpunit
のDokerfile
ではコンテナ起動時にphpunit
を起動すべく設定されています。
Dokerfile
の28行目付近から以下のような記述があります。
# Set up the application directory.
VOLUME ["/app"]
WORKDIR /app
# Set up the command arguments.
ENTRYPOINT ["/usr/local/bin/phpunit"]
CMD ["--help"]
コマンドの意味は以下です。
コマンド | 意味 |
---|---|
VOLUME ["/app"] | /appというディレクトリを作る |
WORKDIR /app | /appに移動(cdだと一旦移動して元に戻るため) |
ENTRYPOINT ["/usr/local/bin/phpunit"] | コンテナ起動時のコマンド実行 |
CMD ["–help"] | ENTRYPOINTの引数(もしrun時に引数があると上書きされる) |
phpunit/phpunit
はrun
の時、以下のようにするよう説明されています。
$ docker run -v $(pwd):/app phpunit/phpunit run
これはカレントディレクトリ(テストファイルかphpunit.xml
が入っている)をコンテナの/appディレクトリにマウントし、phpunit
を実行するという意味になります。
Dokerfile
の中でVOLUME
とWORKDIR /app
が記述されていたのは、テストファイルが入っているホストのディレクトリをマウントするための処理なのデス。
Entrypointの確認・意味の把握
またENTRYPOINT ["/usr/local/bin/phpunit"]
と記述があるので、コンテナ起動時にphpunit
が起動します。
run
時に以下のように何も引数を指定しないとCMD ["--help"]
で指定されているようにヘルプが表示されます。
- 引数なし・ヘルプが表示される
$ docker run -v $(pwd):/app phpunit/phpunit
引数をつけるとCMD ["--help"]
は上書きされて、指定した引数がENTRYPOINT
の引数になります。
/app
を引数として指定・/app
にあるテストファイルのテストが実行される
$ docker run -v $(pwd):/app phpunit/phpunit /app
もしくは
- テスト設定ファイルを指定・
phpunit.xml
で指定したテストが実行される
docker run -v $(pwd):/app phpunit/phpunit --configuration phpunit.xml
これらの仕組みによって、phpunit/phpunit
はコンテナ起動時にphpunit
コマンドが指定した引数で起動し、テストが終了したあとコンテナは終了するようになっています。
テスト環境の準備
MySQLコンテナとPHPUnitコンテナの連携
DBUnitを加えてbuild
したphpunit/phpunit(以下mino/phpunit
)でデータベースに関わるテストをする場合のためにMySQLを用意します。
必ずしもDockerのコンテナでMySQLを用意しないといけないわけではないですが、データベースの差し替えなどを行いやすいのはDockerコンテナの良い所です。
というわけでMySQLもコンテナで用意します。
MySQLは公式イメージとして存在しています。こちらは特にカスタマイズする必要はないので、Dockcerfile
の編集は行いません。
MySQLコンテナとPHPUnitコンテナを連携するにはlink
オプションを使います。
MySQLコンテナの作成・起動
PHPUnitコンテナで連携するには、先にMySQLコンテナが作成・起動されていることが条件になります。
$ docker run -d --name testdb -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_USER_PASSWORD=upass -e MYSQL_USER_NAME=hoge mysql
これでtestdb
というコンテナ名で、hoge
というユーザがスーパーユーザとなったdbというデータベースをもったMySQLコンテナが起動します。
ユーザを作る必要があるのかですが、@MINOにはよくわかりません。通常のサーバならroot
権限でログインするのは良くないと思いますが、コンテナなのでroot
のみでもいいのかなと思います。
後の作業ではroot
でログインしています。
MySQLコンテナの情報取得
先ほどbuild
したmino/phpunit
からPHPUnitコンテナを作成・起動します。としたいのですが、事前に調べておきたいことがあります。
MySQLコンテナの情報を取得しておきたいのです。
Dockerコンテナはデフォルトで(たぶん)172.17.0.0/24
のネットワークに所属します。プライベートアドレスですね。
存在するコンテナの数や起動しているコンテナによってコンテナのIPアドレスが変化する可能性があります。そのため、MySQLコンテナのIPは環境変数から取得するのが望ましいデス。
linkオプションというのは、この環境変数をクライアント側(今回ではPHPUnitコンテナ)に出力するという機能と言えます。
普通のOSコンテナであれば、シェルを起動してコンテナの中に入り確認できますが、今回のPHPUnitコンテナの場合、ちょっと工夫が必要です。
phpunit/phpunit
のDockerfile
を確認してみると、
ENTRYPOINT ["/usr/local/bin/phpunit"]
という記述が或ると思いますが、先程も説明したとおりこれはコンテナ起動時に/usr/local/bin/phpunit
を実行するという意味です。
この記述があるため、コンテナの中に入る為にシェルを引数とする以下のようなrun
ができません。
$ docker run --rm --link testdb -v $(pwd):/app mino/phpunit /bin/bash
これはENTRYPOINT ["/usr/local/bin/phpunit"]
が設定されているために、/bin/bash
を指定しても、/usr/local/bin/phpunit
の引数になってしまうので、シェルは起動されないためです。 (エラーになります)
これを回避するために、コンテナ起動時にENTRYPOINT
の上書きを行うため、--entrypoint
オプションを使います。
$ docker run --rm -it --link testdb -v $(pwd):/app --entrypoint /bin/bash mino/phpunit
この記述でENTRYPOINT ["/usr/local/bin/phpunit"]
は/bin/bash
で上書きされるので、シェルが起動します。i
とt
オプションを忘れずに。(シェルを起動させる為だけなら -v $(pwd):/app
は必要ないです)
PHPUnitコンテナの中に入ったら、env
コマンドで環境変数を確認してみます。
root@696bcb5978ec:/app# env
PACKAGES=php-pear curl
TESTDB_PORT_3306_TCP_ADDR=172.17.0.5
HOSTNAME=696bcb5978ec
TERM=xterm
PHP_INI_DIR=/usr/local/etc/php
TESTDB_PORT_3306_TCP=tcp://172.17.0.5:3306
TESTDB_PORT=tcp://172.17.0.5:3306
TESTDB_ENV_MYSQL_ROOT_PASSWORD=pass
TESTDB_ENV_MYSQL_VERSION=5.7.10-1debian8
COMPOSER_VERSION=1.0.0-alpha11
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GPG_KEYS=1A4E8B7277C42E53DBA9C7B9BCAA30EA9C0D5763
TESTDB_NAME=/modest_fermat/testdb
TESTDB_PORT_3306_TCP_PROTO=tcp
PWD=/app
TESTDB_PORT_3306_TCP_PORT=3306
TESTDB_ENV_MYSQL_MAJOR=5.7
SHLVL=1
HOME=/root
TESTDB_ENV_MYSQL_DATABASE=db
COMPOSER_HOME=/root/composer
PHP_VERSION=7.0.0
_=/usr/bin/env
環境変数の変数名は
コンテナ名の大文字+それぞれの環境変数名
となっています。
今回作ったMySQLコンテナはアドレス(TESTDB_PORT_3306_TCP_ADDR
)が172.17.0.5
であること、データベース名(TESTDB_ENV_MYSQL_DATABASE
)がdb
であることなどがわかります。
環境変数のパターンがわかってしまえば、のぞきにこなくても環境変数をつかえるのですが、この方法でとりあえず接続に必要な情報を得ることができます。
これらの情報を得ることで、データベース接続情報をハードコードしなくて済みます。
PHPでの情報
先ほど確認したMySQLコンテナの情報ですが、PHPUnitコンテナのPHPからphpinfo()
にて確認することができます。先に言えとか言わないの。
以下は出力の一部抜粋です。
PHP Variables
Variable => Value [40/1947]
$_SERVER['PACKAGES'] => php-pear curl
$_SERVER['TESTDB_PORT_3306_TCP_ADDR'] => 172.17.0.4
$_SERVER['HOSTNAME'] => be54672348de
$_SERVER['TERM'] => xterm
$_SERVER['PHP_INI_DIR'] => /usr/local/etc/php
$_SERVER['TESTDB_ENV_MYSQL_USER_PASSWORD'] => upass
$_SERVER['TESTDB_PORT_3306_TCP'] => tcp://172.17.0.4:3306
$_SERVER['TESTDB_PORT'] => tcp://172.17.0.4:3306
$_SERVER['TESTDB_ENV_MYSQL_ROOT_PASSWORD'] => pass
$_SERVER['TESTDB_ENV_MYSQL_VERSION'] => 5.7.10-1debian8
$_SERVER['COMPOSER_VERSION'] => 1.0.0-alpha11
$_SERVER['PATH'] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$_SERVER['GPG_KEYS'] => 1A4E8B7277C42E53DBA9C7B9BCAA30EA9C0D5763
$_SERVER['TESTDB_NAME'] => /sharp_knuth/testdb
$_SERVER['TESTDB_PORT_3306_TCP_PROTO'] => tcp
$_SERVER['PWD'] => /app
$_SERVER['TESTDB_PORT_3306_TCP_PORT'] => 3306
$_SERVER['TESTDB_ENV_MYSQL_MAJOR'] => 5.7
$_SERVER['SHLVL'] => 1
$_SERVER['HOME'] => /root
$_SERVER['TESTDB_ENV_MYSQL_DATABASE'] => db
$_SERVER['TESTDB_ENV_MYSQL_USER_NAME'] => hoge
$_SERVER['COMPOSER_HOME'] => /root/composer
$_SERVER['PHP_VERSION'] => 7.0.0
$_SERVER['_'] => /usr/local/bin/php
$_SERVER['PHP_SELF'] => phpinfo.php
$_SERVER['SCRIPT_NAME'] => phpinfo.php
$_SERVER['SCRIPT_FILENAME'] => phpinfo.php
$_SERVER['PATH_TRANSLATED'] => phpinfo.php
$_SERVER['DOCUMENT_ROOT'] =>
$_SERVER['REQUEST_TIME_FLOAT'] => 1457158144.526
$_SERVER['REQUEST_TIME'] => 1457158144
$_SERVER['argv'] => Array
(
[0] => phpinfo.php
)
SERVER['argc'] => 1
$_ENV['PACKAGES'] => php-pear curl
$_ENV['TESTDB_PORT_3306_TCP_ADDR'] => 172.17.0.4
$_ENV['HOSTNAME'] => be54672348de
$_ENV['TERM'] => xterm
$_ENV['PHP_INI_DIR'] => /usr/local/etc/php
$_ENV['TESTDB_ENV_MYSQL_USER_PASSWORD'] => upass
$_ENV['TESTDB_PORT_3306_TCP'] => tcp://172.17.0.4:3306
$_ENV['TESTDB_PORT'] => tcp://172.17.0.4:3306
$_ENV['TESTDB_ENV_MYSQL_ROOT_PASSWORD'] => pass
$_ENV['TESTDB_ENV_MYSQL_VERSION'] => 5.7.10-1debian8
$_ENV['COMPOSER_VERSION'] => 1.0.0-alpha11
$_ENV['PATH'] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$_ENV['GPG_KEYS'] => 1A4E8B7277C42E53DBA9C7B9BCAA30EA9C0D5763
$_ENV['TESTDB_NAME'] => /sharp_knuth/testdb
$_ENV['TESTDB_PORT_3306_TCP_PROTO'] => tcp
$_ENV['PWD'] => /app
$_ENV['TESTDB_PORT_3306_TCP_PORT'] => 3306
$_ENV['TESTDB_ENV_MYSQL_MAJOR'] => 5.7
$_ENV['SHLVL'] => 1
$_ENV['HOME'] => /root
$_ENV['TESTDB_ENV_MYSQL_DATABASE'] => db
$_ENV['TESTDB_ENV_MYSQL_USER_NAME'] => hoge
$_ENV['COMPOSER_HOME'] => /root/composer
$_ENV['PHP_VERSION'] => 7.0.0
$_ENV['_'] => /usr/local/bin/php
$_ENV
と$_SERVER
でMySQLコンテナの情報が出力されていることがわかります。
つまりこれらの情報を使い、MySQLコンテナのデータベースにつなぐには以下のように記述するとハードコードにならなくて良いかもデス。
PDOでMySQLコンテナのデータベースに接続する際の例です。環境変数を活用して接続しています。
<?php
$DB_DSN = "mysql:dbname={$_ENV['TESTDB_ENV_MYSQL_DATABASE']};host={$_ENV['TESTDB_PORT_3306_TCP_ADDR']}";
$DB_USER = root;
$DB_PASSWD = $_ENV['TESTDB_ENV_MYSQL_ROOT_PASSWORD'] ;
$pdo = new PDO( $DB_DSN, $DB_USER , $DB_PASSWD );
テストの実行
さてやっと環境構築ができたので、テストを書いたら以下を実行してみましょう。
PHPUnitでは****Test.php
という名前のファイルはテストファイルと認識しますので、テストコードファイルを放り込んでいるディレクトリを指定することでテストが実行されます。
$ docker run --rm --link testdb -v $(pwd):/app mino/phpunit /app/tests
もしくはxml設定でテストを行う場合はphpunit.xml
を指定してあげることで、xml設定の通りテストが行われます。
$ docker run --rm --link testdb -v $(pwd):/app mino/phpunit --configuration phpunit.xml
--rm
はコンテナが終了した際についでにコンテナを削除するオプションです。コンテナは削除しない限りいくつでも増えていきます。手動で削除するのは意外と骨が折れます。
今回のような場合ではコンテナはその都度削除されて構いません。そのために--rm
で楽をします。
こんなテストコードでテストしてみました。(データベースにつなぐ必要ないテストですが…)
<?php
require dirname(dirname(__FILE__)) . '/vendor/autoload.php';
class DbTest extends PHPUnit_Framework_TestCase
{
private $db;
public function setUp()
{
$DB_DSN = "mysql:dbname={$_ENV['TESTDB_ENV_MYSQL_DATABASE']};host={$_ENV['TESTDB_PORT_3306_TCP_ADDR']}";
$DB_USER = "root";
$DB_PASSWD = $_ENV['TESTDB_ENV_MYSQL_ROOT_PASSWORD'];
$this->db = new GeneralDb($DB_DSN, $DB_USER, $DB_PASSWD);
}
public function testCheckplaceHolder()
{
$prepearSt = $this->db->checkPlaceHolder("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$this->assertRegExp("/:[a-z0-9]/", $prepearSt);
return $prepearSt;
}
/**
* @depends testCheckplaceHolder
*/
public function testAnalyzedPrepearSt($prepearSt)
{
$placeHolders = $this->db->analyzedPrepearSt($prepearSt);
$this->assertNotEmpty($placeHolders);
}
}
テスト成功しますた。
PHPUnit 5.1.7 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 71 ms, Memory: 4.00Mb
OK (2 tests, 2 assertions)
こんな感じでテストの実行ができます。
テストが終わると、PHPUnitコンテナは--rm
オプションによって削除されますので、ゴミも残りません。気兼ねなくガツガツテストできますね。
git pre-commit hookとの連携
さてgitとの連携も目標にしていました。先に説明したとおり、pre-commit hook機能を使って連携します。
開発しているディレクトリにgitリポジトリを作ります。
$ git init
Initialized empty Git repository in /project
これで/project
内に.git
ディレクトリができているはずです。その中のhooks
ディレクトリのpre-commit.sample
をエディタで開きます。
サンプルの処理がたくさん書かれていますが、一旦消して以下の用に記述します。テスト先の指定(/app/tests
)は各位の環境に合わせてください。もちろんphpunit.xml
の指定でもOKですよ。
シェルスクリプトはまだ覚え始めたばかりなので大目にみてくだちい…。
一応MySQLコンテナの起動確認をしてPHPUnitコンテナを立ち上げるようにしています…。
#!/bin/bash
function testrun(){
docker run --rm --link "$1" -v $(pwd):/app peco/phpunit_pdo --configuration phpunit.xml
}
dbc="testdb"
if [[ -z $(awk "/${dbc}/ { print }" <(docker ps -a)) ]]; then
docker run -d --name ${dbc}
-e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=db -e MYSQL_USER_PASSWORD=upass -e MYSQL_USER_NAME=hoge mysql
testrun ${dbc}
else
if [[ -z $(awk "/${dbc}/ { print }" <(docker ps)) ]]; then
docker start ${dbc}
testrun ${dbc}
else
echo "DB alredy run!!"
testrun ${dbc}
fi
fi
ファイル名は.sample
を外してpre-commit
で保存します。(元のファイルはいろいろ参考になるので残しておいたほうが良いです)
ここまで準備出来たら、あとはコーディングして適切なタイミングでgit commit
するとテストを行うことができます。
さてこれでとりあえず、目標は果たすことができました。DBunitのテストの方法などに関しては本記事と趣旨が異なりますので(@MINOがもう少しマシなレベルになったら)別途記事にしたいと思います。
補足・ハマりポイント
テスト失敗した時の挙動
今回の例ではテストに失敗するとgit commit
は中止されます。
以下はテストに失敗した場合ですが、git commit
は実行されず(commitにメッセージが出ていない)そのままプロンプトに戻っています。
DB alredy run!!
PHPUnit 5.1.7 by Sebastian Bergmann and contributors.
.F 2 / 2 (100%)prepear Analyzed error!
Time: 88 ms, Memory: 4.00Mb
There was 1 failure:
1) DbTest::testAnalyzedPrepearSt
Failed asserting that a NULL is not empty.
/app/test/DbTest.php:27
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
$
テストに成功するとちゃんとcommitされています。最後に2行にステータスが出力されていますよね。
DB alredy run!!
PHPUnit 5.1.7 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 68 ms, Memory: 4.00Mb
OK (2 tests, 2 assertions)
[master 0a96433] test
1 file changed, 1 insertion(+)
$
gitのhook機能はスクリプトの最後にエラーコードが出力された場合、hookが停止されるようになっています。
なので、hookスクリプトで不用意にエラーコードを出さないようにしてしまうと、本当は止まって欲しいhookが動いてしまう場合があります(はず)。
今回の例ではphpunitコマンドがテストに失敗をした際にエラーコードを出しているので、それをhookが拾っているような状態になっています。
git commit時にテストしたくないんだけど
何らかの理由で、git commit
時にテストを行いたくない場合があるかもしれません。そんな時は以下の様にすればOKです。
$ git commit --no-verify
これでpre-commit
フックは沈黙します。
なぜMySQLコンテナには–rmオプションをつけていないのか
公式のMySQLイメージから立ち上げたMySQLコンテナは-d
オプションやシェル起動をさせないと、立ち上がったらすぐ停止してしまいます。
また--rm
は-d
と排他的な関係にあり、-d
を指定していると--rm
は指定できません。
PHPUnitコンテナが立ち上がってテストが終わるまではMySQLコンテナを起動しておかなくてはなりませんが、そのためには-d
オプションが必要になるのです。
そのため、コンテナを自動的に消す--rm
オプションが使えません。
というのが@MINOの見解です。おそらく良いやり方があるような気がしますが、まだ良くわかっていません。
じゃあなんでstopコマンドとrmコマンドでMySQLコンテナを片付けないの?
いろいろテストをしていて、コンテナの停止と削除に少しタイムラグがあることがわかりました。
コンテナの停止は直ぐに終わらないので、安全に終了させる為wait
コマンドを挟んで終了状態になるのを待つことになります。
@MINOの環境ではその待ち時間がちょっと長かったので、いちいち待つのがちょっと鬱陶しいという点があります。
できるだけコンテナの寿命は短い方がいいと偉い人が言っているのですが、これは作業効率とのトレードオフでいいのかなと思っています。
Dockerfileのファイル名
Dockerfile
はファイル名を変えることもできるみたいですが、現状ではあまりメリットがないのか、ほとんど「Dockerfile
」のままです。適宜ディレクトリを分けるなりして管理するのが定石なんでしょうか?
Dockerfile
自体をどのように管理するかは各位の方法によるかと思いますが、おそらくプロジェクトに付随した「環境」としてgit等で管理するのが良いのでは無いかと思います。
build
されたimage
自体はDockerが管理しているディレクトリに収められるので、別途移動などをする必要はありません。
git commitではなく、保存ごとにテストのほうが良い?
どっちが良いんでしょう?
@MINOの場合頻繁に保存をしますので、保存が作業の一区切りにはなっていません。保存タイミングでテストをすると、ちょっと無駄な時間が多いかなという印象です。
例えばvimで保存ごとにgit commit
をするという設定を見かけることがありますが、@MINOとしてはコミットが増えすぎてしまい、逆に整理できなくなりそうな気がしています。
一応任意のタイミングでテストという感じで今はやっていますが、いろいろテストしてみるべきかなと思っています。
後記
随分長い記事になってしまいましたが、ちょっとは参考になったでしょうか?
最初DockerのDの字も知らず、「コンテナ?」と固まっていたのですが、やれば覚えていくものですね。いろいろハマる点はありますが、環境構築は楽しいです。