読者です 読者をやめる 読者になる 読者になる

JenkinsとDockerを使い、WebアプリのE2Eテストをする

Docker Jenkins Codeception Selenium

前回のエントリの続きです。

今回は単純なアプリのテストなので、Dockerfileもテスト手順もシンプルでした。
より複雑な手順が必要となる、Seleniumを利用したWEBアプリのE2Eテストなどもこの方式でできるのかどうか、引き続き実験してみます。

ということで実験しました。

目次

  • 前提環境
  • 実際の手順
    1. WEBアプリを準備する
    2. Dockerコンテナ上でWEBアプリケーションを公開する
    3. Seleniumコンテナを導入、起動する
    4. Codeception+WebDriverの設定をする
    5. テスト実行用のスクリプトを作成する
    6. Jenkinsにジョブを作成し、ビルドする
  • 今後の課題など

前提環境

前回同様

OS
Windows 8:
Virtual Box
4.3.20
Vagran
1.7.2

実際の手順

1. WEBアプリを準備する

Laravel4で、GET /usersするとusersテーブルからユーザー一覧を取得して表示する、という簡単なものを作成しました。
piccagliani/docker-l4-sample · GitHub

テストはCodeceptionのWebDriverモジュールを利用して記述しました。

<?php
$I = new AcceptanceGirl($scenario);
$I->wantToTest('list users');
$I->amOnPage('/users');
$I->see('Bill Evans', '#users tbody tr:nth-child(1)');
$I->see('Thelonius Monk', '#users tbody tr:nth-child(2)');
$I->see('Bud Powell', '#users tbody tr:nth-child(3)');

2. Dockerコンテナ上でWEBアプリケーションを公開する

Dockerファイルをアプリケーションリポジトリ.docker/Dockerfileに記述します。
少し悩んだ点は、ApacheMySQLなどの設定をどのように行えばよいか、ということ。
選択肢として、以下を考えました。

  • RUN命令でsedを使い、必要な箇所の置換を行う
  • ADD命令でファイルを置き換える

前者は、単純な置換であればいいですが、あっという間に未来のないDockerfileになりそうです。
なので後者でいくことにしました。

Dockerfileと並列に、

を用意し、ADDします。

結果、Dockerfileは以下のようになりました。

FROM centos:6

MAINTAINER @piccagliani

RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN echo ZONE="Asia/Tokyo" > /etc/sysconfig/clock
RUN echo UTC="false" >> /etc/sysconfig/clock
RUN source /etc/sysconfig/clock

RUN yum update -y
RUN yum install -y git

RUN rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm
RUN rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

# PHP 5.6
RUN yum install -y --enablerepo=remi --enablerepo=remi-php56 php php-pdo php-mysql php-mbstring php-mcrypt

# Composer
RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer

# MySQL
RUN rpm -Uvh http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
RUN yum install -y mysql-community-server

# Add config files
ADD httpd.conf /etc/httpd/conf/httpd.conf
ADD my.cnf /etc/my.cnf
ADD php.ini /etc/php.ini

# Apache
EXPOSE 80

CMD ["/bin/bash"]

docker runする際に、 -p 8000:80オプションをつけてあげれば、コンテナの外部からホストのIPを指定してWEBアプリケーションにアクセスできるようになります。

3. Seleniumコンテナを導入、起動する

Codeceptionの開発者である@davertさんが、Selenium Server, Xvfb, Firefox, Chromiumを導入済みのコンテナイメージを共有してくれています。
詳細は Acceptance Testing With No Selenium or PhantomJS Installed を確認してください。

$ docker pull davert/selenium-env
$ docker run -d -p 4444:4444 davert/selenium-env

これでバックグラウンドでコンテナが起動している状態となります。

4. CodeceptionのWebDriverモジュールの設定をする

結果から書くと、以下のような設定を行いました。

class_name: AcceptanceGirl
modules:
    enabled:
        - AcceptanceHelper
        - Db
        - WebDriver
    config:
        WebDriver:
            host: 172.17.42.1 # SeleniumサーバーのIP = DockerホストのIP
            port: 4444
            url: 'http://172.17.42.1:8000/' # テスト対象のIP = これまたDockerホストのIP
            browser: firefox
            window_size: 1680x1050
            wait: 10
            capabilities:
                unexpectedAlertBehaviour: 'accept'

ちょっと悩んだのは、以下の設定項目。

url
Starting URL for your app.
host
Selenium server host (127.0.0.1 by default).
port
Selenium server port (4444 by default).

コメントとして書いていますが、WEBアプリのコンテナも、SeleniumEnvコンテナも、-pオプションでポートのマッピングを行っているので、どちらもDockerホストのIPにすればとりあえず動くんじゃね?と。。。

5. テスト実行用のスクリプトを作成する

$APPROOT/.docker/run-tests.shを以下のように作成しました。

#!/bin/bash

service httpd start
service mysqld start

cd /opt/docker-l4-sample
chown -R 777 app/storage
mysql -u root < ./.docker/setup.sql

composer install
vendor/bin/codecept run --debug

前回と同様、/opt/docker-l4-sampleにはJenkinsのジョブのワークスペースがマウントされます。
また、./.docker/setup.sqlはアプリ用のDB・ユーザーの作成を行います。

6. Jenkinsにジョブを作成し、ビルドする

前回と基本的には同じです。

ジョブ名

リポジトリの名前に合わせました。

ジョブの形式

「フリースタイル・プロジェクトのビルド」を選択。

ソースコード管理

Gitを選択し、リポジトリのURL、資格情報を設定。

ビルド手順

「シェルの実行」を選択し、以下を記述。ポートのマッピングオプションを追加しています。

docker build -t $JOB_NAME $WORKSPACE/.docker/
docker run -v $WORKSPACE:/opt/$JOB_NAME -p 8000:80 $JOB_NAME sh /opt/$JOB_NAME/.docker/run-tests.sh

ビルドを実行すると、
コンテナがビルドされ、
run-tests.shが実行され
最終的に、こんな出力となり、

Codeception PHP Testing Framework v2.0.9
Powered by PHPUnit 4.4.2 by Sebastian Bergmann.
  Rebuilding AcceptanceGirl...

Acceptance Tests (1) -----------------------------------------------------------------------
Modules: AcceptanceHelper, Db, WebDriver
--------------------------------------------------------------------------------------------
Trying to test list users (usersCept)                                                  
Scenario:
* I am on page "/users"
* I see "Bill Evans","#users tbody tr:nth-child(1)"
* I see "Thelonius Monk","#users tbody tr:nth-child(2)"
* I see "Bud Powell","#users tbody tr:nth-child(3)"
 PASSED 

--------------------------------------------------------------------------------------------

Functional Tests (1) -----------------------------------------------------------------------
Modules: Filesystem, FunctionalHelper, Db, Laravel4
--------------------------------------------------------------------------------------------
Trying to test list users (usersCept)                                                  
Scenario:
* I am on page "/users"
  [Response] 200
  [Page] http://localhost/users
  [Cookies] []
  [Headers] {"cache-control":["no-cache"],"date":["Fri, 23 Jan 2015 09:49:20 GMT"],"content-type":["text/html; charset=UTF-8"]}
* I see "Bill Evans","#users tbody tr:nth-child(1)"
* I see "Thelonius Monk","#users tbody tr:nth-child(2)"
* I see "Bud Powell","#users tbody tr:nth-child(3)"
 PASSED 

--------------------------------------------------------------------------------------------
  Rebuilding UnitGirl...

Unit Tests (0) ------------------------------
Modules: Asserts, UnitHelper
---------------------------------------------
---------------------------------------------


Time: 1.39 minutes, Memory: 18.00Mb

OK (2 tests, 6 assertions)
Finished: SUCCESS

ビルドが成功しました!

今後の課題など

Jenkinsのジョブが同時に走った場合

今回はSeleniumとWEBアプリ間の通信にホストIPを利用して行いましたが、異なるアプリのE2Eテストが同時に実行された場合を考えると、アプリ毎にポートのマッピングを変えないといけないのではないか?と思います。
Jenkinsにジョブを設定する際に「どのポート空いてるんだっけ?」と考えるのはちょっとイヤですね。
他のホストのコンテナに接続するパターン - ✘╹◡╹✘
などを参考に、コンテナ間の通信はどうあるべきか、について理解する必要がありそうです。


以上となります。
課題はありますが、理想のCIサーバを目指し、また一歩進みました。