2016/10/18 21:02:38

Dockerを使ってMySQLと連携したPHPUnitテスト環境構築にチャレンジしてみた

dokcer
目次(クリックするとジャンプします)
  • 1:ちゃんとテストしたい
  • 2:テスト環境構築の構想
  • 2.1:目標にする項目
  • 3:処理の要や機能などについて
  • 3.1:処理の引き金になるgit commit
  • 3.1.1:hook機能の仕組み・hooksディレクトリの中身
  • 3.1.2:ファイル名がhookタイミングを表している
  • 3.2:Dockerの構成
  • 3.3:テストの処理の流れ
  • 4:テスト環境の構築
  • 4.1:Dockerの導入
  • 4.2:phpunit/phpunitのカスタマイズ
  • 4.2.1:phpunit/phpunitの中身
  • 4.2.2:PHPエクステンションの確認
  • 4.2.2.1:PDOインストール
  • 4.2.3:DBUnitのインストール
  • 4.3:イメージのビルド
  • 4.4:コンテナ起動時の挙動
  • 4.4.1:ボーリュームの設定とマウント
  • 4.4.2:Entrypointの確認・意味の把握
  • 5:テスト環境の準備
  • 5.1:MySQLコンテナとPHPUnitコンテナの連携
  • 5.1.1:MySQLコンテナの作成・起動
  • 5.1.2:MySQLコンテナの情報取得
  • 5.1.3:PHPでの情報
  • 6:テストの実行
  • 7:git pre-commit hookとの連携
  • 8:補足・ハマりポイント
  • 8.1:テスト失敗した時の挙動
  • 8.2:git commit時にテストしたくないんだけど
  • 8.3:なぜMySQLコンテナには--rmオプションをつけていないのか
  • 8.4:じゃあなんでstopコマンドとrmコマンドでMySQLコンテナを片付けないの?
  • 8.5:Dockerfileのファイル名
  • 8.6:git commitではなく、保存ごとにテストのほうが良い?
  • 9:後記

ちゃんとテストしたい

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-commitcommitメッセージが入力される前、つまりコミット前に実行されます。

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がいいような気がしています。

今回は以下のimageDockerfileを使います。

  • phpunit/phpunit
  • mysql(公式)

テストの処理の流れ

まとめるとこんな感じで処理が進むことをイメージしています。

  • テストコードを書く
  • テスト対象コード書く
  • commitする pre-commit hookを起動させる
  • pre-commit hookで指定されたスクリプトを実行
  • スクリプトによりDockerのコンテナが立ち上がりphpunitphpunit.xmlによって実行される
  • テスト成功したらcommit実行へ
  • テスト失敗したらcommit中止へ(コードの修正へ)

これを繰り返してコーディングしていこうということデス。

テスト環境の構築

Dockerの導入

Dockerの導入は事前に行ってください。以下は公式のドキュメントです。

Docker公式のインストールガイド(英語)
左側のメニュー→Docker Engine →install→各OSの説明 Mac、WindowはもちろんLinuxは主要ディストリビューションのほとんどのインストール方法解説を網羅しています。

また拙ブログでもインストールに関して記事にしていますので参照してください。

DockerEngineをインストールする

phpunit/phpunitのカスタマイズ

ここではPHPのテストワークフレームであるPHPUnitMySQL とを連携する際に必要になるいくつかの設定を追加していきます。その際Dockerfile を編集することになります。

Dockerfileimage をビルドする為の設計図みたいなものです。

コンテナの中に入って必要になるツールなどをインストールする手もあるのですが、作業としては意外と面倒です。また手動になってしまうので、再現性に欠けます。

環境構築をDockerfileに記述することで、環境の再現性・可搬性が持てます。そして何より楽です。

まずはDocker上のPHPUnitイメージであるphpunit/phpunitのGitHubリポジトリからDockerfileをゲットしましょう。このphpunit/phpunitPHPUnit開発者謹製です。

phpunit/phpunit のGitHubリポジトリは以下です。今回は(記事執筆時2016/2現在)最新バージョンである5.1.0を使いたいと思います。

phpunit/phpunitリポジトリ

任意のディレクトリにcloneしてゲットします。

$ git clone https://github.com/JulienBreux/phpunit-docker.git

phpunit/phpunitの中身

phpunit/phpunitDockerfile を見て行きましょう。

phpunit/phpunitcomposer/composer というimage がベースになっています。名前からわかる通りPHPのパッケージ管理ソフトのComposerです。

composer/composer 自体は公式のPHP が元になっています。PHP はさらにdebian:jessieが元になっています。

つまりphpunit/phpunitPHPComposerPHPUnitをインストールした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/composerDockerfileには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/phpunitDockerfileでは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したimagepdo_mysqlDBUnitがインストールされた状態になります。

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/phpunitDokerfileではコンテナ起動時に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/phpunitrun の時、以下のようにするよう説明されています。

$ docker run -v $(pwd):/app phpunit/phpunit run

これはカレントディレクトリ(テストファイルかphpunit.xmlが入っている)をコンテナの/appディレクトリにマウントし、phpunitを実行するという意味になります。

Dokerfileの中でVOLUMEWORKDIR /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/phpunitDockerfileを確認してみると、

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で上書きされるので、シェルが起動します。itオプションを忘れずに。(シェルを起動させる為だけなら -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の字も知らず、「コンテナ?」と固まっていたのですが、やれば覚えていくものですね。いろいろハマる点はありますが、環境構築は楽しいです。