1. Automated Testing
2011/05/09(月) 24:17 Javascript親記事へこのエントリーをはてなブックマークに追加

FirebugなどJavaScriptデバッガーはいろいろあって便利だけど、Humans are lazyだ。
デバッガーによるデバッグは手動で行っているので、そこにAutomated testingをもうけて自動化しよう。

JavaScriptの自動テストのソフトウェアの歴史は2001年のJsUnitまでさかのぼる。
2004年にSeleniumがでてきて、 JavaScript/web developmentは今も活気があるコミュニティだ。

1.1 The Unit Test

Unitテストはコンポーネント単体で実行すべきであり、他のテストに依存するべきではない。
場合によっては単体で実行できるようにするためにmockやstubといったものを必要にするときがあるだろう。(この辺はPart III, Real-World Test-Driven Development in JavaScript で)

いつUnitテストを実行するのかを書き出してみると
  • 実装が完了したとき、正しい挙動しているかを確認
  • 実装を変更したとき、挙動が変わってないかを確認
  • 新しいUnitテストを追加したとき、それが本来の目的を満たしているかを確認

1.1.1 Unit Testing Frameworks

まだUnitテストをしたことないと思っていても実際には似たような事をやっている場合もあります。(コンソールでインスペクトして確認するなど)
ただしこれは自動化されていないし、また再利用ができていません。
xUnit test caseという定式化されたものを学んでいく。
xUnitはJUnitをポートした、もしくはそれのアイデアをベースとしたテストフレームワークの事を言います。
もっと正確にいうとSUnitというSmalltalkのテストフレームワークのアイデアとコンセプトがベースにあります。@ Kent Beck

1.1.2 strftime for JavaScript Dates

多くのプログラミング言語はタイムスタンプを出力する strftime 関数に似たものを持っているので、これを例にする。
JavaScriptには無いので作る…
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();
        },
        //  Format  shorthands
        F: "%Y-%m-%d",
        D: "%m/%d/%y"
    };
    return strftime;
}());
このDate.prototype.strftimeは大きく分けるとreplace関数とDate.formatsからできていて、
次のように分解できる
  • Date.formats はキーと日付から対応するデータを値として抽出するメソッドようなフォーマット指定子を持つオブジェクト。
  • いくつかのフォーマット指定子は便利なショートカットを持ってる
  • String.prototype.replace はフォーマット指定子にマッチする正規表現%([a-zA-Z])と一緒に使う
  • 置換関数は指定子がDate上で利用可能かをチェックする。
これらについてどうテストする?
一個づつコンソールに入れてやっていくのは大変だ...
>>>  var  date  =  new  Date(2009,  11,  5);
>>>  date.strftime("%Y");
"2009"
>>>  date.strftime("%m");
"12"
>>>  date.strftime("%d");
"05"
>>>  date.strftime("%y");
"9"
毎回これをやるのは大変だから、小さなHTMLページを作ってそこに書いていこう。
jsファイルを読み込むHTMLを作ってjsにはコンソールに出力するように書いておけば、毎回入力しなくてもみることができるだろう
var  date  =  new  Date(2009,  11,  5);
console.log(date.strftime("%Y"));
console.log(date.strftime("%m"));
console.log(date.strftime("%d"));
console.log(date.strftime("%y"));
console.log(date.strftime("%F"));

1.2 Assertions

Unitテストの中心はassertionだ。
1.1ではコンソールに出力して、目視によってそれが正しいかを確認していたが、Unitテスト内ではassertionを使って自動的にそれらをチェックするようにする。
function assert(message, expr) {
    if (!expr) {
        throw new Error(message);
    }
    assert.count++;
    return true;
}
assert.count = 0;
このassert関数は第二引数がtruthy(false, null, undefined, 0, "", NaNではない)かをチェックするシンプルなもの。
これを先ほどのコンソール出力の代わりにする。
var date = new Date(2009, 9, 2);
try {
    assert("%Y  should  return  full  year", date.strftime("%Y") === "2009");
    assert("%m  should  return  month", date.strftime("%m") === "10");
    assert("%d  should  return  date", date.strftime("%d") === "02");
    assert("%y  should  return  year  as  two  digits", date.strftime("%y") === "09");
    assert("%F  should  act  as  %Y-%m-%d", date.strftime("%F") === "2009-10-02");
    console.log(assert.count + "  tests  OK");
} catch (e) {
    console.log("Test  failed:  " + e.message);
}
先ほどより若干量が増えたが、テスト自体がどこがおかしいを自動的に知らせてくれるようになった。

1.2.1 Red and Green

Unitテストの世界では"red","green"という言葉が"失敗","成功"の代わりに使われている。
下のように出力する際にDOMに色を付けて視覚的にするのもいいかも
function  output(text,  color)  {
    var  p  =  document.createElement("p");
    p.innerHTML  =  text;
    p.style.color  =  color;
    document.body.appendChild(p);
}
//  console.log  can  now  be  replaced  with
output(assert.count  +  "  tests  OK",  "#0c0");
//  and,  for  failures:
output("Test  failed:  "  +  e.message,  "#c00");

1.3 Test Functions, Cases and Suites

さきほどのassert関数ではテストが失敗したときにエラーをthrowするだけでした。
なので、よりよりフィードバックをするようにtestCaseという関数に一連の動作をまとめてみる。
function testCase(name, tests) {
  assert.count = 0;
  var successful = 0;
  var testCount = 0;
  var hasSetup = typeof tests.setUp == "function";
  var hasTeardown = typeof tests.tearDown == "function";

  for (var test in tests) {
    if (!/^test/.test(test)) {
      continue;
    }

    testCount++;

    try {
      if (hasSetup) {
        tests.setUp();
      }

      tests[test]();
      output(test, "#0c0");

      if (hasTeardown) {
        tests.tearDown();
      }

      // If the tearDown method throws an error, it is
      // considered a test failure, so we don't count
      // success until all methods have run successfully
      successful++;
    } catch (e) {
      output(test + " failed: " + e.message, "#c00");
    }
  }

  var color = successful == testCount ? "#0c0" : "#c00";

  output("<strong>" + testCount + " tests, " +
         (testCount - successful) + " failures</strong>",
         color);
}
このtestCaseを使って、strtimeをテストする。
/*globals assert, testCase*/
testCase("strftime test", {
  setUp: function () {
    this.date = new Date(2009, 9, 2, 22, 14, 45);
  },

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

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

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

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

  "test format shorthand %F": function () {
    assert("%F should be shortcut for %Y-%m-%d",
           Date.formats.F === "%Y-%m-%d");
  }
});
setupで共通の処理を書けば、重複したコードをテストに入れないで良くなる。

1.3.1 Setup and Teardown

xUnitフレームワークは大抵setUpとtearDownというメソッドを提供します。
先ほどの例のようにtestCaseを渡して、テストを実行する前にsetUpメソッドが実行されるので、共通の処理に必要なものを先に定義できます。
tesrDownは逆にテストが終わった後に実行し、後片付けなどに使われます。
testCase("strftime test", {
  setUp: function () {
    this.date = new Date(2009, 9, 2, 22, 14, 45);
  },

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

1.4 Integration Tests

Unitテストがwheel, wheels, electric windows などの部品だとすると、Integrationはそれらをくみ上げた車。
シンプルな Integration TestsはUnitテストに似ている。

1.5 Benefits of Unit Tests

"Writing tests is an investment."
テストを書くのは時間がかかるという反論もあるけど、自動化したテストにより繰り返しテストが行える。

1.5.1 Regression Testing

回帰テスト
人は時に間違いを犯すため製品にバグが混入することがある。
最悪な時はそれを直した後にまた再発することがある。
それを避けるための手助けをするのがRegression Testingだ。
自動化されたテストは過去に戻りテストを行うこともできる。

1.5.2 Refactoring

リファクタリングは動作を維持したまま実装を変更する必要があります。
リファクタリングには失敗しやすいポイントがたくさんあります。変数名を変えたときに参照してるのを変え忘れる、スコープの領域など…
"変更するコードセクションの固定テストセットを作成せよ" via Refactoring: Improving the Design of Existing Code

1.5.3 Cross-Browser Testing

大変な労力を払う部分だが
シンプルなUnitテストを作っておけば後はクレバーな test runnerがあればいけるはず。

1.6 Pitfalls of Unit Testing

Unitテストを書くのは必ずしも簡単ではない。
いいテストを書くためには練習が必要です。悪いテストを書いたらテストの利点が得られない。
真によいUnitテストを書くためには、テストしやすいコードが必要になるでしょう。
この書籍ではテストしやすいコードと、よいUnitテストについて示していくつもりだ。

おわり

結構読みにくい(英語力が足りない的な意味で)本かもしれない。
普通のJavaScriptのコード部分がほとんどテストコードなので、そう感じるのかもしれないな。
ただ、分かりにくいという意味ではない。
テスト例は順を追っていけるようにgitでバージョン管理されたものも配布されている。

名前:   

  • TB-URL  http://efcl.info/adiary/0115/tb/