Edited at

JSでもCucumber

More than 5 years have passed since last update.

国内・海外問わずあまり使用例の聞かないcucumber-jsですが、個人的によく使うので使い方を紹介したいと思います。

cucumber-jsを利用しているプロジェクトとしては以下があります:

他にcucumber.jsを利用しているプロジェクトをご存知の方いましたら教えてください。


はじめに

cucumberについて詳細はWikipediaに譲りますが、簡単にいえばGherkin syntaxでfeatureを定義してこれに対応するステップを実装し受け入れテストを実行します。オリジナルはrubyですがそのJavaScriptバージョンがcucumber-jsです。

ここではcucumber-jsとselenium-webdriverを用いて実際にfeatureとstep、これをサポートするスクリプトファイルを記述して動かしていってみます。実コードは以下にあります。

https://bitbucket.org/p_baleine/cucumber-sample/src


サンプルアプリ

サンプルなので単純に、/にアクセスすると「Hello, cucumber」を表示するアプリとします。以下アプリ(lib/app.js):

ar express = require('express');

var multiline = require('multiline');
var app = express();

app.get('/', function(req, res) {
res.send(multiline(function() {/*
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<p>Hello, cucumber</p>
</body>
</html>
*/
}));
});

module.exports = app;


最初のfeature

最初といってもこのドキュメント中では唯一のfeatureですが、featuresディレクトリを作成して、以下内容を記載したfeatures/hello-world.featureファイルを作成します。

Feature: Hello, world

cucumber.jsの利用者として
cucumber.jsの使い方を知るために
トップページにアクセスして出力されるHTMLを検証する

Scenario: トップページへアクセス
Given トップページへアクセスする
Then 「Hello, cucumber」と表示される


stepをサポートするスクリプト

stepを実装する前にstepをサポートするスクリプトを作成します。まずstepの実行環境となるworldを定義するファイルfeatures/suppoer/world.jsを以下内容で作成します

var url = require('url');

var sw = require('selenium-webdriver');
var SeleniumServer = require('selenium-webdriver/remote').SeleniumServer;
var server = new SeleniumServer('~/bin/selenium-server-standalone-2.40.0.jar', { port: 4444 });
var chai = require('chai');
var chaiWebdriver = require('chai-webdriver');
var PORT = 3001;

function World(callback) {
this.app = require('../../lib/app');
this.server = null;
callback();
}

// サーバ起動
World.prototype.bootServer = function(callback) {
this.server = this.app.listen(PORT, callback);
};

// サーバシャットダウン
World.prototype.shutdownServer = function(callback) {
this.server.close(callback);
};

// webdriver起動
World.prototype.startDriver = function() {
server.start();
this.driver = new sw.Builder()
.usingServer(server.address())
.withCapabilities(sw.Capabilities.chrome())
.build();
chai.use(chaiWebdriver(this.driver));
};

// webdriver終了
World.prototype.quitDriver = function() {
this.driver.quit();
};

// `path`にブラウザでアクセスする
World.prototype.visit = function(path, callback) {
this.driver.get(this.url(path));
callback();
};

// urlを返却する
World.prototype.url = function(path) {
return url.format({
protocol:'http',
hostname: 'localhost',
port: PORT,
pathname: path
});
};

exports.World = World;

長い…アプリサーバの起動、終了と検証用ブラウザ(selenium-webdriver)の起動、終了用のメソッドを実装しておきます。今回は検証用のブラウザをselenium-webdriverで準備します、ブラウザの選択については最後の方に少し記述します。

scenarioの前や後に実行したいコードはfeatures/support/hooks.jsに記述すると実行されるようになります、ここでfeatures/support/world.jsで定義したメソッドをコールします。

module.exports = function() {

this.Before(function(callback) {
this.startDriver();
this.bootServer(callback);
});

this.After(function(callback) {
this.quitDriver();
this.shutdownServer(callback);
});
};


stepファイル

ここまでで一度cucumber.jsを実行してみます:

# selenium-serverを起動しておく

# $ java -jar ~/bin/selenium-server-standalone-2.40.0.jar
$ ./node_modules/.bin/cucumber.js
UU

1 scenario (1 undefined)
2 steps (2 undefined)

You can implement step definitions for undefined steps with these snippets:

this.Given(/^トップページへアクセスする$/, function (callback) {
// express the regexp above with the code you wish you had
callback.pending();
});

this.Then(/^「Hello, cucumber」と表示される$/, function (callback) {
// express the regexp above with the code you wish you had
callback.pending();
});

まだstepが一つも実装されていないので、実装すべきstepが出力されます、features/step_definitions/hello-world.step.jsに出力された内容をコピー&ペーストします。

ここでもう一度cucumber.jsを実行すると各ステップの状態が未実装からペンディングに変わります。ここから各ステップを実装していきます。↓は実装後のコード:

var World = require('../support/world').World;

var expect = require('chai').expect;

module.exports = function() {
this.World = World;

this.Given(/^トップページへアクセスする$/, function (callback) {
this.visit('/', callback);
});

this.Then(/^「Hello, cucumber」と表示される$/, function (callback) {
expect('p').dom.to.contain.text('Hello, cucumber')
.then(function() {
callback();
});
});
};

ステップの実装の内容は利用するフレームワークに依存するものになります。今回はselenium-webdriverとchai-webriverを利用した例となります。ここまで記述して再度cucumber-jsを実行すると各ステップがパスするはずです。


タグ

cucumberに記述する内容はいわゆる受け入れテスト系のテストなので、各シナリオを実行するのに時間がかかるかと思います。タグを利用することで特定のシナリオだけ実行することができるようになります。具体的には以下のように

@dev

Scenario: トップページへアクセス
Given トップページへアクセスする
Then 「Hello, cucumber」と表示される

シナリオの上に@dev等のように記述してcucumber実行時に-tオプションでこれを指定することでタグの付いているシナリオだけ実行されます:

$ ./node_modules/.bin/cucumber.js -t @dev

前述のhookは特定のタグのときにだけ実行されるように指定することもできます:

this.Before('@someCondition', function(callback) {

this.loadFixtures(callback);
});


cucumber-jsで使うブラウザ

今回はselenium-webdriverを利用しましたが以前はzombieをよく使っていました。cucumber-jsの公式ページでもzombieを利用した例が記述されています。どれも一長一短かと思いますが、個人的な感想:


zombie

Nodeで実装されたテスト用のブラウザ(みたいなもの)です、よくメンテナンスされていて、何よりもcucumber-jsと同一プロセス上でnodeのプログラムとして動くのでブラウザのhttpリクエストをモックできます、なのでFacebookやTwitterのOAuth認証なんかもモックしてテストすることができます。以下は以前書いていたFacebook認証のモック用のコードです、これをFacebook認証を必要とするシナリオのフックで実行します

// Facebookのリクエストをmock

World.prototype.mockFacebookRequest = function() {
var code = 'mycode';
var accessToken = 'mytoken';

nock('https://www.facebook.com')
// ログインダイアログ
.get('/dialog/oauth?' + querystring.stringify({
response_type: 'code',
scope: 'email',
redirect_uri: config.facebook.redirectUri,
client_id: config.facebook.clientId
}))
// コールバックにリダイレクト
.reply(303, '', {
'Location': this.url('/sessions/facebook-callback') + '?code=' + code
})
.reply(200, '');

nock('https://graph.facebook.com')
// アクセストークンリクエスト
.get('/oauth/access_token?' + querystring.stringify({
code: code,
client_id: config.facebook.clientId,
client_secret: config.facebook.clientSecret,
redirect_uri: config.facebook.redirectUri
}))
.reply(200, {
access_token: accessToken,
expires: new Date().getTime()
})
// API呼び出し
.get('/me?' + querystring.stringify({ access_token: accessToken, locale: 'ja_JP' }))
.reply(200, {
id: this.REGISTERED_USER_DATA.facebook_id
});
};

ここでモックにはnockを用いています。

ただときどきクライアントサイドのJSが動かなかったりして、まぁそこからzoombieのissueとか漁りに行ったりもするんですが、いかんせんデバッグ対象がリアルなブラウザではなくNode上で動くほぼユーザのいないブラウザってところが結構萎えるのが欠点かと思います、まだIEデバッグしてるほうがイイ気もする。


selenium-webdriver

なので最近はこちらを使ってみています。chai-webdriverを一緒に使えばかなり素直にexpectationも書けるし、windowリサイズしないとvisibleなはずのものが見えないとかみたいな変なバグ踏まなければ(今朝これで2時間くらい悩んだ、そんなんわからないって)(←はPhantomJSの問題です、webdriverは悪くありませんでした…ぼけてるなぁ)、実ブラウザで動いてくれるのはやはり快適かと思います。


最後に

自分はcucumberでまず実装する予定の機能を検証して(最初は実装がないのでテストが落ちて)から、サーバサイドであればmocha、クライアントサイドであればkarma(+ mocha)でユニットテストを書いたりしています。まぁここら辺は個人とかプロジェクトの趣味みたいなものかと思いますが、cucumber側でユーザが実際に触る観点でテストを記述できると、じゃぁユニットテスト側ではMとかCのインタラクションをテストすれば良い、という風に切り分けの見通しが良くなって良いかなと思います。