エンジニアサポートCROSS 2014 レポート

「実況解説つき!ペアプロでわかるJavaScriptテスト入門」レポート

2014年1月17日、ベルサール新宿グランドにて開催されたエンジニアサポートCROSS 2014の中の1セッション実況解説つき!ペアプロでわかるJavaScriptテスト入門をレポートします。

エンジニアサポートCROSS 2014とは

複数の技術を身につけなければWebサービスは作れない=クロスしないと生きていけないをテーマに、⁠エンジニアサポート新年会2012 CROSS」として第一回が2012年に開催された勉強会イベント、それがCROSSです。今年で3回目になります。

「実況解説つき!ペアプロでわかるJavaScriptテスト入門」

このセッションはJavaScriptで書かれたよくあるコードをベースに、ペアプロでテストコードを足していく様子を解説者が説明するという内容になります。

それでは、さっそくセッションの模様を見ていきましょう。

画像

登壇者

本セッションの登壇者は、セッションオーナーの吾郷協さん@kyo_ago⁠、本セッションで実際にコードを書く初心者役として参加の古屋徹さん、書かれたコードの解説者として参加の佐藤鉄平さん@teppeisと和田卓人さん@t_wadaです。

吾郷さん
画像
古屋さん
画像
佐藤さん
画像
和田さん
画像

はじめに

本セッションで使われたソース一式は吾郷さんがGitHub上にアップロードしています。

ダウンロードしたファイルを確認してみると、testem.jsonには今回使用するtestemというフレームワークの設定情報が記述されています。

testem.json
{
        "src_files": [
                "js/lib/*.js",
                "test/lib/*.js",
                "setup.js",
                "js/*.js",
                "test/*.js"
        ],
        "test_page": "tests.mustache"
}}

setup.jsはテストが開始されるときに一番最初に一度だけ呼ばれるものです。ここではテストコードをわかりやすくするために単純にtrueを返すだけのupdateLabel()という関数が記述されており、これが読み出されるかをテストしていきます。mocha.setup()Mochaというフレームワークも利用するので、そのための記述となります。

setup.js
function updateLabel() {
        return true;
}
mocha.setup('bdd');

また、/js/ディレクトリ内にテストしたいjsファイルが、/test/ディレクトリ内にテストするためのjsファイルが含められています。

準備

まずはコマンドラインからtestemのインストールを行います。

npm -g install testem

インストールができたらtestemコマンドを打ってtestemを起動させましょう。testemはJavaScriptを実際にウェブブラウザ上で実行してOK/NGを返してくれます。http://localhost:7357/にサーバが立ち上がるのでウェブブラウザでアクセスすると下記のような画面が表示されます。

画像

これは既に用意されていた/test/0.jsの結果が返ってきている状態となります。

/js/0.js
function firstTest() {
        return true;
}
/test/0.js
describe('0', function() {
        it('trueが返ってくるか確認する', function() {
                expect(firstTest()).to.eql(true);
        });
});

テストツールについて

ここで、マイクをバトンタッチして佐藤さんからJavaScriptのテストツールについての解説がありました。

JavaScriptのテストツール
画像

佐藤さん曰く「昔はJavaScriptでテストは書きにくかったですが、最近は様々なツールが発達してきて書きやすくなってきた」とのことです(上図参考⁠⁠。

今回使っているのはこの図でいう3つ。具体的には次のものです(実はモックライブラリのSinon.JSも使っていますがそれは後述⁠⁠。

  • リモートテストランナーとしてのtestem
  • テスティングフレームワークとしてのMocha
    • describe/itというようなテスト形式で書いてそれを実行するというところをやってくれる
  • アサーションライブラリとしてのexpect.js
    • expect(window.r).to.be(undefined);のような形で、window.rはundefinedですよというのをアサーションする(確認する)

また、和田さんからは、他の言語だと例えばJavaだとJUnitなど支配的なテスティングフレームワークがありますが、JavaScriptだと流行りのものが移ろいやすく、現在はJasmineとMochaの二強状態。Jasmineは全部入りで、Mochaは自分の好きなものを組み合わせて使える、という違いがあるという話がありました。

なぜtestemが必要かということについては、JavaScriptのテストの難しいところは必ずブラウザから確認する必要があり、testemを使うと実際のブラウザに対してテストでき、IEでもFirefoxでもChromeでも、もちろんモバイルのブラウザからも確認が可能となるということです。

例題1

それでは本題。よくある、ボタンイベントを取得するテストです。

/js/1.js
$(function() {
        $('.jQuery.button').click(function() {
                updateLabel();
        });
});

これに対するテストコードはこちら。

/test/1.js
// 筆者注:describeでこのテストがどういうテストかを宣言
describe('1', function() {
        // 筆者注:itでこのテストがどういうルールによって展開して何を確認したいかを宣言
        it('ボタンをクリックしてupdateLabelを表示する', function() {
                // 筆者注:updateLabel()が実行されたかは外からはわからないので、
                // それがわかるように別のfunctionに置き換える。
                // Sinon.JSというライブラリを利用
                sinon.stub(window, 'updateLabel');

                // 筆者注:testInitというfunctionを実行、jQueryの$関数を一時的に置き換える(後述)
                var init = window.testInit.args.shift();
                init[0]();
                $('.jQuery.button').click();

                expect(updateLabel.called).to.eql(true);
                window.updateLabel.restore();
        });
});

ここで、本来であれば元のコードに全く手を加えずにテストできるのが一番良いですが、jQueryを使っている場合はそれが難しいので少し手を加えるそうです。

setup.jsに次の一文を追加します。

window.testInit = sinon.stub();

また、/js/1.jsの一行目を次のように書き換えます。

(window.testInit || $)(function() {

これによって、testInitが定義されている場合はtestInitを使って初期化をし、そうでなければ$を使って初期化をするようになります。本来であれば読み込まれてすぐに$関数で初期化されてしまうのを別のもので初期化するようにしています。

この状態でブラウザを見てみると、2つ目のテストの結果(1.jsのテストの結果)が返ってきているのがわかります。

画像

ここで使ったSinon.JSというライブラリは、このような既存のメソッドを置き換えるモックライブラリやテストダブルライブラリと言われているものであり、JavaScriptのテストではデファクトになっていると佐藤さんから解説がありました。

和田さんからもモックライブラリについての詳しい解説があり、簡単に言うとテストしにくい仕組みをテストしやすい偽物で置き換えてテスト可能な形にしていくためのものと言及していました。

例題2

次はこれまたよくあるタイマーを用いたコードを見ていきます。

/js/02..js
(window.testInit || $)(function() {
        setTimeout(function() {
                updateLabel();
        }, 10);
});

10msだけなら待つことは簡単ですが、例えば3分待つといったコードがある際に実際にその時間待たなくてもテストできる仕組みがあるそうで、ここではその解説がメインとなりました。

/test/02.js
describe('2', function() {
        it('非同期でupdateLabelを表示する', function() {
                sinon.stub(window, 'updateLabel');

                // 筆者注:時間を一時的に仮のものに置き換える
                var fakeclock = sinon.useFakeTimers();
                var init = window.testInit.args.shift();
                init[0]();
                // 筆者注:仮に置き換えた時間を100ms進める
                fakeclock.tick(100);

                expect(updateLabel.called).to.eql(true);
                window.updateLabel.restore();
                fakeclock.restore();
        });
});

このように、Sinon.JSを用いることでコード内の時間も自由に扱うことが可能となるそうです。sinon.useFakeTimers()によりJavaScript世界での時間が止まり(時間が勝手には進まなくなる、setTimeout()setInterval()date()などが止まる)、また、.tick()で時間を自由に進められるようになるとの解説が佐藤さんからありました。

また、JavaScript自体が非同期な言語であるためにこういったテストはよくあるという話の中、上記のような例のほかにMochaを使った次のような例も解説しました。

describe('User', function() {
        describe('#save()', function() {
                // 筆者注:引数にdoneというものをとって、
                it('should save without error', function(done) {
                        var user = new User('Luna');
                        // 筆者注:例えばsaveという非同期な関数(setTimeoutなど)
                        user.save(function(err) {
                                if (err) throw err;
                                // ここでdone()が実行されるまではこのテストケースが終了しない
                                done();
                        });
                })
        })
})

非同期のテストをする際には基本的にはsinon.useFakeTimers()をオススメしますが、コールバック形式のAPIをとる関数を使うときのテストなどでこのdoneがかなり使いやすいということです。

例題3

その次に、テストが大変なajaxを用いたコードです。

/js/3.js
(window.testInit || $)(function() {
        $.get('/').done(function(result) {
                updateLabel(result);
        });
});

これのテストをそのままやろうとすると、サーバから値が返ってこないとダメで、そうするとJavaScriptだけでテストが完結できません。また、サーバにファイルを上げたり、サーバがレスポンスをきちんと返してこないとテストできません。そこで、テストできる形に置き換えて、テストできるようにします。

/test/3.js
describe('3', function() {
        it('APIレスポンスをupdateLabelする', function() {
                // 筆者注:サーバにアクセスしている部分をダミーに置き換える
                var fakeserver = sinon.fakeServer.create();
                // 筆者注:置き換えたサーバがどういう振る舞いをするかを設定
                fakeserver.respondWith('/', [200, {
                        'Content-Type': 'text/plain'
                }, 'result']);

                sinon.stub(window, 'updateLabel');

                var init = window.testInit.args.shift();
                init[0]();
                // 筆者注:サーバからレスポンスを返してくる、という動作を記述
                fakeserver.respond();

                expect(updateLabel.called).to.eql(true);
                window.updateLabel.restore();
                fakeserver.restore();
        });
});

コード中、sinon.fakeServer.create();によってhttpリクエストが内部的に書き換えられて、実際には通信を行わずこちらから指示をしない限りは何もしない状態になっています。また、このコードのように事前にrespondWithの内容を設定する方法のほかに、respondの中で書くこともできるそうです。

例題4

最後に、これまでの例題の総復習のような例題が出ました。

/js/4.js
(window.testInit || $)(function() {
        $('.all.button').click(function() {
                $(this).find('.form').slideToggle(300, function() {
                        updateLabel();
                });
        });
        $('form').on('submit', function(e) {
                e.preventDefault();
                $.post('/', $(this).serialize()).done(function(result) {
                        updateLabel(result);
                });
        });
});

この時点で残念ながら時間切れとなってしまい、これの回答はセッション中は行われませんでしたが、前述の吾郷さんのGitHubにこれまでの回答も含め収められてますのでぜひみなさんも確認してみてください(吾郷さん親切!⁠⁠。

テストケースを日本語を書くことについて

ここで、佐藤さんから「テストケースを日本語で書くことについて」の是非の話がありました(上記で見てきたように、本セッションではitの中で日本語でテストケースを書いていますね⁠⁠。

これに対する和田さんの回答は「実行上問題ないのであれば、かつ日本人中心のチームであれば積極的に日本語使っていきましょう」とのことでした。佐藤さんからも「英語だからってテストを書くハードル高くなるのはつまらないよね」という実際的な話もありました。

まとめ

今回のセッションではJavaScriptでほとんどテストを書いたことが無い人が今日からでもユニットテストが始められるように、testem/Mochaなどのフレームワークを用いたテストのやりかたについて実際のコードに触れながら一からの解説が行われました。

実は、冒頭で佐藤さんから会場に対して「JavaScriptでテストを書いたことがある人はどのくらいいますか?」という質問があり、書いたことがある人は会場の5割ほど、また、ユニットテストを当たり前にやっている人は1割ほどという結果でした。

記事の中で見てきたようにテストのための使いやすそうなツールがこれだけある中、本セッションによって(またはこの記事によって)JavaScriptでテストを書く人が増え、より良いコードがこの世に増えていく助けになれば幸いです。

(私もきちんとテストがんばろう…と思います。)

おすすめ記事

記事・ニュース一覧