処理待ちをより簡単に行えるようになったUxU 0.7.6をリリースしました - 2010-01-29 - ククログ

ククログ

株式会社クリアコード > ククログ > 処理待ちをより簡単に行えるようになったUxU 0.7.6をリリースしました

処理待ちをより簡単に行えるようになったUxU 0.7.6をリリースしました

2010年1月29日付で、テスティングフレームワークUxUのバージョン0.7.6をリリースしました。

新機能のutils.wait()について

今回のアップデートでの目玉となる新機能は、非同期な機能のテストをより簡単に記述できるようにするヘルパーメソッドであるutils.wait()です。これは、以下のように利用します。

function testSendRequest() {
  myFeature.sendRequest();
  utils.wait(1000); // 1000ミリ秒=1秒待つ
  assert.equals('OK', myFeature.response);
}

function testLoad() {
  var loaded = { value : false };
  content.addEventListener('load', function() {
    content.removeEventListener('load', arguments.callee, false);
    loaded.valeu = true;
  }, false);
  myFeature.load();
  utils.wait(loaded); // valueがtrueになるまで待つ
  assert.equals('OK', content.document.body.textContent);
}

また、utils.wait()は関数の中でも利用できます。

function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    utils.wait(1000);
    assert.equals(aExpected, myFeature.response);
  }
  assertSend('OK', '...');
  assertSend('NG', '...');
  assertSend('?', '...');
}

utils.wait()が受け取れる値は、これまでの処理待ち機能で yieldに渡されていた値と同じです。詳しくは処理待ち機能の利用方法をご覧下さい。大抵の場合、yield Do(...);と書かれていた箇所は、utils.wait(...);へ書き換えることができます。

ただし、このヘルパーメソッドはFirefox 3以降やThunderbird 3以降など、Gecko 1.9系の環境でしか利用できません。Thunderbird 2などのGecko 1.8系の環境ではエラーとなりますのでご注意下さい。(それらの環境でもテストの中で処理待ちを行いたい場合は、従来通りyieldを使用して下さい。)

これまでの処理待ち機能の特徴と欠点

これまでUxUでは、「機能を実行した後、N秒間待ってから、機能が期待通りに働いたかどうかを検証する」「初期化処理で、ページの読み込みの完了を待ってから次に進む」といった処理待ちを実現する際は、JavaScript 1.7以降で導入されたyieldを使う仕様となっていました。

yieldを含む関数はジェネレータとなり、関数の戻り値をイテレータとして利用できるようになります。この時ジェネレータの内側から見ると、「yieldが出現する度に処理が一時停止する」という風に考えることができます。

function gen() {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  yield 'step2 done';
  alert('step3');
}
var iterator = gen(); // この時点ではまだ関数の内容は評価されない
var state;
state = iterator.next(); // 'step1' が表示される
alert(state);            // 'step1 done' が表示される
state = iterator.next(); // 'step2' が表示される
alert(state);            // 'step2 done' が表示される
try {
  state = iterator.next(); // alert('step3'); が実行される
} catch(e if e instanceof StopIteration) {
  // 次のyieldが見つからないので、StopIteration例外が投げられる
}

UxUに従来からある処理待ち機能は、この考え方を推し進めて作られています。テスト関数の中にyieldがある場合(つまり、関数の戻り値がイテレータとなる場合)は、フレームワーク側で自動的にイテレーションを行い、yieldに渡された値をその都度受け取って、次にイテレーションを行うまでの待ち条件として利用しています。例えば、数値が渡された場合はその値の分の時間だけ待った後で次にイテレーションを行う、といった具合です。

このやり方の欠点は、yieldを含む関数から任意の戻り値を返すことができないという点です。

function gen() {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  return 'complete';
}
try {
  var iterator = gen();
} catch(e) {
  alert(e); // TypeError: generator function gen returns a value
}

returnを書くと、関数の実行時にエラーになってしまいます。どうしても何らかの値を取り出したい場合は、値を取り出すためのスロットとなるオブジェクトを引数として渡すなどの工夫が必要になります。

function gen(aResult) {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  aResult.value = 'complete';
}
var result = {};
var iterator = gen(result);
var state;
state = iterator.next(); // 'step1' が表示される
alert(state);            // 'step1 done' が表示される
try {
  state = iterator.next(); // 'step2' が表示される
} catch(e if e instanceof StopIteration) {
  alert(result.value); // 'complete' が表示される
}

また、ジェネレータは実行してもその段階では関数の内容が評価されないという点にも注意が必要です。例えば以下のようなテストは、期待通りには動いてくれません。

function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    yield 1000;
    assert.equals(aExpected, myFeature.response);
  }
  assertSend('OK', '...');
  assertSend('NG', '...');
  assertSend('?', '...');
}

この例では、assertSend()を実行したことで戻り値としてイテレータが返されているものの、そのイテレータに対するイテレーションが一切行われていないため、リクエストも行われなければアサーションも行われないということになってしまっています。これは以下のように、返されたイテレータをそのままフレームワークに引き渡して、フレームワーク側でイテレーションを行わせる必要があります。

function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    yield 1000;
    assert.equals(aExpected, myFeature.response);
  }
  yield assertSend('OK', '...');
  yield assertSend('NG', '...');
  yield assertSend('?', '...');
}

また、このままではジェネレータの中で発生した例外のスタックトレースを辿れないという問題もあります。スタックを繋げるためには、ヘルパーメソッドのDo()を使ってyield Do( assertSend('OK', '...') )のように書かなければなりません。

utils.wait()を使う場合、これらのことを考慮する必要はありません。冒頭のサンプルコードのように、素直に書けば素直に動作してくれます。

スレッド機能を使った処理待ち

utils.wait()がどのように実装されているかについても解説しておきます。

このメソッドの内部では、Gecko 1.9から実装されたスレッド関連の機能を利用しています。

window.setTimeout(function() {
  alert('before');
}, 0);
alert('after');

JavaScriptは基本的にシングルスレッドで動作するため、このようにタイマーを設定すると、その処理はキューに溜められた状態となります。その上で、メインの処理が最後まで終わった後でやっとキューの内容が処理され始めるため、この例であれば「after」「before」の順でメッセージが表示されることになります。

var finished = false;
window.setTimeout(function() {
  alert('before');
  finished = true;
}, 0);
var thread = Cc['@mozilla.org/thread-manager;1']
              .getService()
              .mainThread;
while (!finished) {
  thread.processNextEvent(true);
}
alert('after');

Gecko 1.9以降のスレッド機能を使うと、メインの処理を一旦中断して先にキューに溜められた処理の方を実行し、その後改めてメインの処理に戻るということができます。実際に、こちらの例では「before」「after」の順でメッセージが表示されます。UxU 0.7.6ではこれを応用して、任意の条件が満たされるまでthread.processNextEvent(true);でメインの処理を停止し続けることによって、処理待ちを実現しています。

なお、HTML5にもWeb Workersというスレッド関係の機能がありますが、こちらは別スレッドでスクリプトを動作させる機能しか持っていないため、上記のようなことは残念ながらできません。

まとめ

UxU 0.7.6からは、utils.wait()を使ってより簡単に処理待ちを行えるようになりました。Firefox 3以降やThunderbird 3以降専用にアドオンを開発する際には、是非利用してみて下さい。