CI as a Service

Travis CIを始めとするウェブサービスとして使えるCIを使って、
JavaScriptのブラウザテスト(ブラウザ上でJavaScriptを走らせて行うユニットテスト)をやる方法をサービスごとにまとめてみました。

テストフレームワークとして Buster.JS を使用して行います。

Karma (旧Testacular) では公式サイトにも Karma – Travis CI でCI Serviceとの連携方法が記載されているのでそちらも参考にして下さい。

今回紹介するCI Servicesは以下のものです。


テスト実行の流れ

Jepso CI を除いては、テスト実行の流れ自体は同じなので先に解説します。

  1. Capture用のローカルサーバを立てる
  2. テストしたいブラウザで capture URL へアクセスする
  3. コマンドラインからテストを実行する
  4. Captureされてるブラウザでテストを実行した結果が得られる

これはJsTestDriverの流れを組んでいるTest Runnerには大体共通してると思います。

この一連の流れをCIサービス上で実行して、Githubにpushするたびに自動でテストを行うようにするのが今回の目的です。

どのCIサービスも裏側ではUbuntu上で動いていて、apt-getやnpmを使ってテスト環境を事前に準備する必要があります。
また、Travis CIdrone.ioには最初からNode.js環境なども用意されているので、BuildHiveに比べると事前の準備は簡潔に済みます。

travis-ci.org Travis CI

Travis CIでは.travis.ymlという設定ファイルを書くことで、テスト環境を揃えることができます。

サンプルプロジェクトは以下においてあります。

テスト環境のセットアップ

サンプルの Browser_CI_as_a_Service / .travis.yml を見ていきます。

env:
- DISPLAY=:99.0
before_script:
- sh -e /etc/init.d/xvfb start
- sh .travis/scripts/setup_busterjs.sh
script:
- npm test
language: node_js
node_js:
- 0.8

Travis CIではデフォルトでNode.js(npm)、PhantomJS、Firefoxの環境が入っているため、before_scriptでインストールするものは、Buster.JSのみとなります。

Travis CIにデフォルトで入っているものは以下で見られます

.travis.yml内に language: node_js を記述しておくと、自動で npm install を実行してくれるので、
レポジトリの Browser_CI_as_a_Service / package.jsondevDependenciesBuster.JS を追加しておきます。

{
    "author" : "azu",
    "name" : "Browser_CI_as_a_Service",
    "description" : "Buster.JS with CI Service example",
    "version" : "0.0.1",
    "scripts" : {
        "test" : "node_modules/.bin/buster-test -r specification"
    },
    "devDependencies" : {
        "buster" : "~0.6"
    },
    "engines" : {
        "node" : "~0.8"
    }
}

これで、自動で npm install が実行されれば、テスト環境が揃います。

テストの実行を設定

.travis.yml の事前準備の部分を見ていきます

env:
- DISPLAY=:99.0
before_script:
- sh -e /etc/init.d/xvfb start
- sh -e .travis/scripts/setup_busterjs.sh

before_script: で実行しているsetup_busterjs.sh は以下のような内容になってます。

#!/bin/sh

node_modules/.bin/buster-server & sleep 5 firefox http://localhost:1111/capture & sleep 5 phantomjs node_modules/buster/script/phantom.js http://localhost:1111/capture & sleep 5 if [ -x "google-chrome" ]; then google-chrome –no-default-browser-check –no-first-run –disable-default-apps http://localhost:1111/capture & sleep 5 fi

ここでやっていることを簡単にまとめると、以下のとおりです。

  1. GUIテスト用にxvfbをstartする(before_script:)
  2. Capture用のローカルサーバ(buster server)を立てる(setup_busterjs.sh)
  3. Firefox/PhantomJSでCature URLにアクセスする(setup_busterjs.sh)

詳しくはTravis CI: GUI & Headless browser testing on travis-ci.orgを参照してください。

後は、テストを実行するだけです。

.travis.ymlの script:npm test を実行するようにしているので、npm testでBuster.JSのテストが実行されるように package.json に設定します。
(language: node_jsが設定されるなら省略可能です)

script:
- npm test

package.json に以下のように書いて、npm test でBuster.JSのテスト実行されるようにしました。

"scripts" : {
        "test" : "node_modules/.bin/buster-test -r specification"
    }

Travis CIの有効化

最後に、Travis CIとGithubのレポジトリを連携させます。

Repositories から テストを走らせたいレポジトリを選んでONにします。

Travis CI  Free Hosted Continuous Integration Platform for the Open Source Community 2013 03 20 17 31 36

Travis CI側で有効にすると、Github側のService Hooksが自動で有効になるので、後はGithubへPushすれば自動でCIが走るようになります。

Travis CIではTravis CI ClientというCLIクライアントがあり、CLIから特定のレポジトリのCIの実行状態や手動での再実行などTravis CIで行う操作をCLI上から行うことができます。

Travis CI Clientは実行結果の取得や過去の履歴などもちゃんと見られるので結構便利です。

より詳しい設定方法等は以下を参照して下さい

drone.io drone.io

次は、 drone.io でのテスト環境を揃える方法についてです。

drone.ioではGithub/Bitbucket/Google Codeと連携できますが、今回は先ほどと同様のレポジトリを使うのでGithubの場合です。

また、drone.ioもNode.js環境が用意されていますが、PhantomJSとGoogle Chromeを追加でインストールして、
Firefox/PhantomJS/Google Chromeの3つでテストを動かしたいと思います。

Admin  azu 2013 03 20 18 43 45

drone.ioもTravis CIと同様にオープンなコードなら無料で殆ど利用できるようになっています。
(前月ぐらいまでは soft limitsな50回/monthの制限が書いてありましたがUnlimitedになってました)

テスト環境のセットアップ

drone.ioはTravis CIのように設定ファイル(.travis.yml)は用意する必要なく、直接ウェブでテスト環境を準備するスクリプトを書くようになっています。

なので、最初にGithubのレポジトリからdrone.ioにプロジェクトを作成します。

まず、 New Project から Githubを選択します。

Add Repo  drone io 2013 03 20 19 08 02

レポジトリの一覧から、CIをしたいレポジトリを選択して、言語の選択ではNode.jsを選択します。
(言語とかは後から変更できるので適当でも大丈夫です)

そうするとビルドスクリプトを書く画面になるので、テスト環境を準備していきます。

Build Setup  Browser CI as a Service 2013 03 20 19 42 58

Commandsの部分にビルドスクリプトを書いていきます。

sudo start xvfb
npm -d install
sh -e .travis/scripts/install_phantomjs.sh
sh -e .travis/scripts/install_chrome.sh
# start buster server
node_modules/.bin/buster-server &
sleep 5
# start browsers
firefox http://localhost:1111/capture &
sleep 5
phantomjs node_modules/buster/script/phantom.js http://localhost:1111/capture &
sleep 5
google-chrome --no-default-browser-check --no-first-run --disable-default-apps http://localhost:1111/capture &
npm test

npm install でBuster.JSをインストールして、
PhantomJS(バイナリでインストールしたかったのでnpm経由)とGoogle Chrome(apt-getで取得してインストール)しています。

scriptはazu/Browser_CI_as_a_Service · GitHubにまとめてあります。
(jubianchi/travisci-helper · GitHub
こちらのスクリプトも参考になります)

後は、Travis CIと同じでbuster serverを起動して、ブラウザでCapture URLにアクセスして、
npm test でテストを動かすという感じです。

Travis CIの setup_busterjs.sh を実行させようと思ったのですが、なぜかテストが成功しても終了しないという感じになったので直接書いています。

今回紹介したビルドスクリプトは Build Setup | Browser_CI_as_a_Service から参照できます。

drone.io はTravisCIよりスマートな感じなので、初めてCIサービスを試す場合はこちらのほうが試行錯誤しやすいかもしれません。

buildhive.cloudbees.com BuildHive

基本的にJenkinsの画面なので、人によっては馴染みやすいかもしれません。
BuildHiveもdrone.ioと同じく、ウェブ上でビルドスクリプトを書いてテスト環境を揃えます。

デフォルトでNode.js環境がないため、そこから環境を揃えていく必要があります。
ただし、Travis CIやdrone.ioと違って、一回ごとに仮想環境がリセットされないで継続する感じなので、少しセットアップ方法が変わります。

Add Git Repository からGithubのプロジェクトを選んで有効化します。

BuildHive Cloud Continuous Integration 2013 03 20 20 32 57

設定 を 選ぶとシェルスクリプトを書く欄が出てくるので、この部分にビルドスクリプトを書いていきます。

export PATH=$HOME/.nodebrew/current/bin:$PATH
if [ ! -x "$HOME/.nodebrew/current/bin/nodebrew" ]; then
    curl https://raw.github.com/hokaccha/nodebrew/master/nodebrew | perl - setup
    nodebrew install-binary stable
    nodebrew use stable
fi

if [ -e /tmp/.X1-lock ]; then rm /tmp/.X1-lock # たまにロックがかかって死ぬので fi if [ -z "$DISPLAY" ]; then export DISPLAY=:1 Xvfb :1 & fi npm -d install node_modules/.bin/buster-server & sleep 5 firefox http://localhost:1111/capture & sleep 5 node_modules/phantomjs/bin/phantomjs node_modules/buster/script/phantom.js http://localhost:1111/capture & sleep 5

npm test

Node.jsはnodebrewを使ってバイナリからインストールしています。

後は、他のものと同じでXvfbをstartして、Capture用のローカルサーバを起動して、Capture URLにアクセスさせて、
npm test するという感じです。

正直、ちゃんとした書き方がよくわからなかったので、適当な部分が多いです。
(Node.jsのアップデートとかも対応したい… buildhive for node)

毎回リセットされない前提で書く必要があるので、どなたかがもう少し良いものを書いてくれるはず。

jepso-ci.com Jepso CI

最後は Jepso CI についてです。
他のCIサービスが特にJavaScriptに限定されないのに対して、Jepso CIはHTMLとJavaScriptというものに限定される作りになっているので全く別の仕組みです。

Saucetestling-ci の方に近いジャンルのCIサービスです。

また、まだ安定してるとは言えないので、実験的でテスト結果も安定しないのもあるため実用で使うのは諦めて下さい。
(2013-03-20)

サンプルプロジェクトはazu/BusterJS_JepsoCI · GitHubに置いてあります。

仕組み

他のCIサービスとは異なる仕組みで、基本的にローカルサーバなどは使わないで、
QUnitのようなテストを走らせるための静的なHTMLファイルと、そのファイルへのパスを書いた .jepso-ci.json という設定ファイルだけです。

今回のプロジェクト(azu/BusterJS_JepsoCI · GitHub)ではルートに、テストを走らせるための test.html をおいてるので、
.jepso-ci.jsonは以下のような内容が入ります。

{
    "url": "/test.html"
}

次に、BusterJS_JepsoCI / test.html を見ていきます。

Jepso CIのテストの成否判定は、test.html内の2つのプロパティによって判定されます。

window.testsPassed;// bool
window.completedTests;// number

Buster.JSのtest runnerとなる test.html は以下のような感じです。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>Buster.JS</title>
    <link rel="stylesheet" href="http://cdn.busterjs.org/releases/latest/buster-test.css">
    <script type="text/javascript" src="http://cdn.busterjs.org/releases/latest/buster-test.js"></script>
<span class="c">&lt;!-- jepso-ci --&gt;</span>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">&quot;text/javascript&quot;</span><span class="nt">&gt;</span>
    <span class="c1">// start test</span>
    <span class="nx">buster</span><span class="p">.</span><span class="nx">testRunner</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&quot;suite:start&quot;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">completedTests</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">testsPassed</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="c1">// each passed test</span>
    <span class="nx">buster</span><span class="p">.</span><span class="nx">assertions</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&quot;pass&quot;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">completedTests</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="c1">// each failure test</span>
    <span class="nx">buster</span><span class="p">.</span><span class="nx">assertions</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&quot;failure&quot;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">testsPassed</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="c1">// finish all test</span>
    <span class="nx">buster</span><span class="p">.</span><span class="nx">testRunner</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&quot;suite:end&quot;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">testsPassed</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">ok</span><span class="p">;</span><span class="c1">// result boolean</span>
    <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span>
<span class="c">&lt;!--test source--&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">&quot;./src/hello.js&quot;</span> <span class="na">type=</span><span class="s">&quot;text/javascript&quot;</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">&quot;./tests/async-test.js&quot;</span> <span class="na">type=</span><span class="s">&quot;text/javascript&quot;</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">&quot;./tests/hello-test.js&quot;</span> <span class="na">type=</span><span class="s">&quot;text/javascript&quot;</span><span class="nt">&gt;&lt;/script&gt;</span>

</head> <body> </body> </html>

結論をいうと、test.htmlを実行した結果 window.testsPassedtrue ならテストが通った事になります。
window.completedTests は非同期のテストがある場合に、++していくとtimeoutを伸ばしてくれるためにあるようです。

実際の処理内容を見ていくと、Buster.JSは .onテスト実行時のイベントを設定できるので、以下のような処理をイベントに仕込んでいます。

buster.testRunner.on("suite:start", function… はテスト開始時に初期化
buster.assertions.on("pass", function... でテストが通るたびにインクリメント
buster.assertions.on("failure", function... で一つでも失敗したらテスト失敗
buster.testRunner.on("suite:end", function... でテスト終了時にテスト結果を代入

後は、テストファイルを読み込ん実行するだけです。
(単純に*.jsファイルをscriptタグで読み込んでる)

Jepso CIとGithub連携

Githubとの連携はPost-Receive Hooksの”WebHook URLs”に https://jepso-ci.com/api/hook と設定するだけです。

後は、GithubにpushすればJepso CIが動作するようになります。
Jepso CIのページは https://jepso-ci.com/<user>/<repogitory> にアクセスすると表示されます。

HTMLを使ったとてもシンプルな仕組みとなっていて、ビルドステータスもSVGで書かれていたりして面白いサービスだと思います。

公式にも幾つかサンプルが用意されています。

Jepso CIは静的なHTMLでテストを動かしてるので、以下のテンプレートも参考になるかもしれません。

また、testling-ciもHTMLをベースとしたテストができるようになったようです。(2013-03-21)

FAQ

Travis CIやBuildHiveではChromeは使えないの?

Travis CI がUbuntu-64bitに移行した際にChromeが動かなくなっています。(インストールはできます)

Chromium/Chrome does not work in an OpenVZ container · Issue #938 · travis-ci/travis-ci のIssueが解決するまでは使えないです。

追記 : Travis CIとChromeについて

記事ではChromeはインストールしていませんでしたが、以下のような変更を入れることで、
Travis CI上でもChromeを動作させることができます。

具体的には以下のようなことが必要になります。

  1. /dev/shm のパーミッションを修正する
sudo chmod 1777 /dev/shm 
  1. Chromeの起動引数に --no-sandboxつける

こうすることで、Travis CIの仮想マシン上でもGoogleChromeを動かすことができるようになるので、
Chromeでテストを実行させることが可能です。(--no-sandbox の副作用が何かあるかもしれませんが)


BuildHive では sudo が使えなかったので、上手くインストール出来ませんでしたが、上手くやればインストールできるかもしれません

もっといろんなブラウザのバージョンで試したいんだけど?

今回紹介した Travis CIdrone.ioBuildHive はブラウザに特化した作りでは無いので、自前でバージョンごとのブラウザを用意すればできますが、負荷が大きそうなのでそういう用途は避けましょう。(また全体として成功か失敗かなのであんまりそういう用途ではない気がする)

BrowserStacktestling-ciSauce Labs などブラウザに特化したサービスを利用するか自前のサーバ等でやりましょう。

CI Servicesの環境に何が入ってるかもっとしりたい

公式サイトにサーバのスペックは書いてあると思います。

デフォルトに入ってるものについてはTravis CIはtravis-ci/travis-cookbooks · GitHubを見ると大体わかります。

gildegoma/travis-ci-inspector · GitHubのように走査して何が入ってるか調べる地道な方法も利用できます。

(対話側のコンソールみたいなのが欲しいですね…)

Example Project

今回使用したサンプルプロジェクトは以下のURLで公開しています。

Test Tools

今回は Buster.JS を使用しましたが、Test Toolとしては他に以下のようなものもあります。

Special Thanks