Fork me on GitHub

小さなJSのテストを書いてみよう

strftime

指定形式にフォーマットされた日時を取得できる関数 strftime を例にテストを書いてみます。

参考

busterjs-kumite/strftime at develop · azu/busterjs-kumite · GitHub
cd `git rev-parse --show-toplevel`/strftime/ 作成したサンプル
Test-Driven JavaScript Development
Buster.JSの作者が書いた書籍。strftimeをchapter1,2で扱ってる
テスト駆動JavaScript
上記の書籍の日本語版

buster.js(設定ファイル) を作る

strftimeは特に環境固有の機能に依存しなくても実現できるため、 Node、ブラウザどちらの環境でも実行できるハイブリッドテストとして定義する。

/busterjs-kumite/strftime/buster.js

var config = module.exports;

config["My node test"] = {
    env : "node", // or "browser"
    tests : [
        "test/*-test.js"
    ]
};

config["My browser test"] = {
    env : "browser", // or "browser"
    sources : [
        "src/*.js"
    ],
    tests : [
        "test/*-test.js"
    ]
};

Nodeとブラウザ向けの設定をそれぞれ書いておくが、両者の違いとしては Node向けの設定ではsourcesの指定がなく、代わりにテストファイル自体に読み込むソースを定義している点が異なっている。

/busterjs-kumite/strftime/test/strftime-test.js

1
2
3
4
5
if (typeof require == "function" && typeof module == "object"){
    buster = require("buster");
    // Nodeの場合はbuster.jsのsourcesでは読み込まれない
    require("../src/strftime");
}

Node、ブラウザ環境 どちらで実行するかは テストの実行環境について で書かれているように、 buster test 実行時に -e/--environment オプション指定すればよい。

テストの実行

手動で実行する場合は、strftimeディレクトリ直下で buster test でテストを実行します。

$ buster test

毎回確認する度に、 buster test を実行するのは手間なので、ファイルの変更を監視して保存される度に buster test を実行してくれる buster autotest コマンドがあります。

$ buster-autotest

実装

strftime.jsの実装

/busterjs-kumite/strftime/src/strftime.js

// strftimeはDataオブジェクトを拡張する
if (typeof Date.prototype.strftime !== "function"){
    Date.prototype.strftime = (function(){
        function strftime(format){
            var date = this;

            return (format + "").replace(/%([a-zA-Z])/g, function(m, f){
                var formatter = Date.formats && Date.formats[f];

                if (typeof formatter == "function"){
                    return formatter.call(Date.formats, date);
                }else if (typeof formatter == "string"){
                    return date.strftime(formatter);
                }

                return "%" + f;
            });
        }

        // Internal helper
        function zeroPad(num){
            return (+num < 10 ? "0" : "") + num;
        }

        Date.formats = {
            // Formatting methods
            d : function(date){
                return zeroPad(date.getDate());
            },

            m : function(date){
                return zeroPad(date.getMonth() + 1);
            },

            y : function(date){
                return zeroPad(date.getYear() % 100);
            },

            Y : function(date){
                return date.getFullYear() + "";
            },

            j : function(date){
                var jan1 = new Date(date.getFullYear(), 0, 1);
                var diff = date.getTime() - jan1.getTime();

                // 86400000 == 60 * 60 * 24 * 1000
                return Math.ceil(diff / 86400000);
            },

            // Format shorthands
            F : "%Y-%m-%d",
            D : "%m/%d/%y"
        };

        return strftime;
    }());
}

strftime-test.js の実装

/busterjs-kumite/strftime/test/strftime-test.js

// テストケース
buster.testCase("Date strftime tests", {
    setUp : function(){
        this.date = new Date(2009, 9, 2, 22, 14, 45);
    },

    "test format specifier %Y" : function(){
        assert.same(Date.formats.Y(this.date), "2009",
                "%Y should return full year");
    },

    "test format specifier %m" : function(){
        assert.same(Date.formats.m(this.date), "10",
                "%m should return month");
    },

    "test format specifier %d" : function(){
        assert.same(Date.formats.d(this.date), "02",
                "%d should return date");
    },

    "test format specifier %y" : function(){
        assert.same(Date.formats.y(this.date), "09",
                "%y should return year as two digits");
    },

    "test format shorthand %F" : function(){
        assert.same(Date.formats.F, "%Y-%m-%d",
                "%F should be shortcut for %Y-%m-%d");
    }
});

それぞれのテストごとに、new Dateするのは冗長なので、 setUpthis.date = new Date(); のようにして行なっておけば、 それぞれのテスト実行前にsetUpが実行されて、 this.date で参照できるため共通部品がまとめられます。

assert.same=== の厳密比較演算子での比較結果がtrueならテストがPassされます。

つまり、イメージ的には以下のような感じです。

// assert.same(Date.formats.Y(this.date), "2009");
// ==>
if(Date.formats.Y(this.date) === "2009"){
  // pass test
}else{
  // fail test
}

やや、おさらい的な内容でしたが、これで短めなstrftimeの実装は終わりです。 一般的なstrftimeにはもっとformatsがあるので、続けてそれを実装してみるのもいいでしょう。

参考

tokuhirom/strftime-js
テストも書かれてるstrftimeの実装

今回、どのような手順で実装したのかは、このアプリのコミットまとめておいたのでそちらで見られるようにしてあります。

簡単に手順を並べると

  1. buster.js(設定ファイル) を書く
  2. strftime.js と strftime-test.js を作成して配置
  3. strftime.js にインターフェイスだけ(中身は実装してない)を実装 (先にインターフェイスだけ作ってるのは、テストを書くときにエディタから補完が効くので形だけ作っておいてる)
  4. strftime-test.js にテストを実装
  5. テストを実行しながら、 strftime.js に中身を実装

というような手順で実装しましたが、これが正解だというものは無いと思うのでまずは書いてみるのがいいでしょう。