script.aculo.usを読み解く

第10回 unittest.js(後編)

この記事を読むのに必要な時間:およそ 12 分

BDDスタイルのテスト記述

BDDはビヘイビア駆動開発の略で,このライブラリでは以下のように記述します。JavaScript自体の制限から,RubyのRSpecほどエレガントでないところもありますが,これまでより読みやすいのが特徴です。こちらにRSpecの解説記事があります。

0001:Test.context("BDD-style testing",{
0002:  
0003:  setup: function() {
0004:  },
0005:  
0006:  teardown: function() {
0007:  },
0008:  
0009:  'should automatically add extensions to strings': function(){
0010:    'a'.shouldEqual('a');
0011:    'a'.shouldNotEqual('b');
0012:    'a'.shouldNotBeNull();
0013:    'a'.shouldBeA(String);
0014:    
0015:    var aString = 'boo!';
0016:    aString.shouldEqual('boo!');
0017:    aString.shouldBeA(String);
0018:    aString.shouldNotBeA(Number);
0019:  },
0020:})

それではコードに戻りましょう。

0508:// *EXPERIMENTAL* BDD-style testing to please non-technical folk
0509:// This draws many ideas from RSpec http://rspec.rubyforge.org/
0510:
0511:Test.setupBDDExtensionMethods = function(){
0512:  var METHODMAP = {
0513:    shouldEqual:     'assertEqual',
0514:    shouldNotEqual:  'assertNotEqual',
0515:    shouldEqualEnum: 'assertEnumEqual',
0516:    shouldBeA:       'assertType',
0517:    shouldNotBeA:    'assertNotOfType',
0518:    shouldBeAn:      'assertType',
0519:    shouldNotBeAn:   'assertNotOfType',
0520:    shouldBeNull:    'assertNull',
0521:    shouldNotBeNull: 'assertNotNull',
0522:    
0523:    shouldBe:        'assertReturnsTrue',
0524:    shouldNotBe:     'assertReturnsFalse',
0525:    shouldRespondTo: 'assertRespondsTo'
0526:  };
0527:  var makeAssertion = function(assertion, args, object) { 
0528:   	this[assertion].apply(this,(args || []).concat([object]));
0529:  }
0530:  
0531:  Test.BDDMethods = {};   
0532:  $H(METHODMAP).each(function(pair) { 
0533:    Test.BDDMethods[pair.key] = function() { 
0534:       var args = $A(arguments); 
0535:       var scope = args.shift(); 
0536:       makeAssertion.apply(scope, [pair.value, args, this]); }; 
0537:  });
0538:  
0539:  [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each(
0540:    function(p){ Object.extend(p, Test.BDDMethods) }
0541:  );
0542:}
0543:

508~543行目のTest.setupBDDExtensionMethodsは,BDDスタイルのテスト記述を実現するための関数です。RSpecから多くの着想を得ています。

512~526行目は,BDDスタイルのメソッド名と,Test.Unit.Assertionのメソッド名の対応付けです。

527行目のmakeAssertionは,assertionにTest.Unit.Assertionのメソッド名を,argsにBDDスタイルのメソッドの引数,objectにメソッドを呼んだオブジェクトを取って,実際のTest.Unit.Assertionの関数をapplyで呼ぶ関数です。

531行目で,BDDのメソッドを全て挙げるためのハッシュテーブルを用意します。

532~537行目で,BDDスタイルのメソッドの中身を作ります。実体は,BDDスタイルのメソッドの呼び出し元のオブジェクトと,その引数の順序を組み換えて,Test.Unit.Assertionの関数を呼ぶようになっています。

539~541行目で,BDDスタイルになるように,配列,文字列,数字,ブール値のそれぞれにメソッドを追加します。

0544:Test.context = function(name, spec, log){
0545:  Test.setupBDDExtensionMethods();
0546:  
0547:  var compiledSpec = {};
0548:  var titles = {};
0549:  for(specName in spec) {
0550:    switch(specName){
0551:      case "setup":
0552:      case "teardown":
0553:        compiledSpec[specName] = spec[specName];
0554:        break;
0555:      default:
0556:        var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
0557:        var body = spec[specName].toString().split('\n').slice(1);
0558:        if(/^\{/.test(body[0])) body = body.slice(1);
0559:        body.pop();
0560:        body = body.map(function(statement){ 
0561:          return statement.strip()
0562:        });
0563:        compiledSpec[testName] = body.join('\n');
0564:        titles[testName] = specName;
0565:    }
0566:  }
0567:  new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
0568:};

544~568行目のTest.contextは,これまでTest.Unit.Runnerで実行していたテストを,BDDスタイルに書くための関数です。引数のnameにテスト全体の名前を,specにテストの記述を,logにログを出力する要素のDOM idを取ります。

545行目で,前述のsetupBDDExtensionMethodsを呼んで準備をします。

551,552行目で,setup,teardownの処理をspecから取りだします。

555行目以降が本質的な部分です。

556行目で,テスト名をBDDスタイルからJavaScript風に変換します。具体的には,'should automatically add extensions to strings'が'testShouldAutomaticallyAddExtensionsToStrings'になります。

557行目の,bodyとは,関数の中身のことを指します。関数をtoStringで文字列にして,splitで文字列の配列にしてから,このような処理をして関数の中身を取り出します。

はじめの状態です。

function()
{
  'a'.shouldEqual('a');
  'a'.shouldNotEqual('b');
  'a'.shouldNotBeNull();
  'a'.shouldBeA(String);
}

557行目のslice(1)で,最初の'function()'を取り除きます。

{
  'a'.shouldEqual('a');
  'a'.shouldNotEqual('b');
  'a'.shouldNotBeNull();
  'a'.shouldBeA(String);
}

558行目のslice(1)で,最初の'{'を取り除きます。

この処理は'function(){'の書法ならば必要ないので,正規表現でチェックしています。

  'a'.shouldEqual('a');
  'a'.shouldNotEqual('b');
  'a'.shouldNotBeNull();
  'a'.shouldBeA(String);
}

559行目のpop()で,最後の'}'を取り除きます。

  'a'.shouldEqual('a');
  'a'.shouldNotEqual('b');
  'a'.shouldNotBeNull();
  'a'.shouldBeA(String);

560行目のstatement.strip()で,全体から空白を取り除きます。

'a'.shouldEqual('a');
'a'.shouldNotEqual('b');
'a'.shouldNotBeNull();
'a'.shouldBeA(String);

563行目で,こうして得られた結果の,文字列の配列を結合し,文字列にして完了です。後で,Test.Unit.Runnerの内部でevalを呼んで,関数に解釈します。

564行目で,テストのタイトルを設定します。

567行目で,これらをTest.Unit.Runnerに渡します。

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記