ClearCode - 株式会社クリアコード

UxU用のテストケースの書き方

テストケースの書き方

UxUは、特定のルールに則って記述されたJavaScriptファイルをテストケースとして実行します。

テストケースとしての内容を含むスクリプトのファイル名は、「<テスト名>.test.js」という命名規則に則って付けることをお勧めします。UxUは特定のフォルダ内に含まれているテストを一括実行する機能を含んでいますが、この命名規則に則っておくと、UxUはテストケースとして実行可能なファイルだけを適切に認識することができます。

テストケースの実行コンテキストでは、UxUによって定義されたヘルパーメソッドが利用できます。詳細はテストケース内で利用可能なヘルパーメソッドをご覧下さい。

実際にUxU用に書かれたテストケースの例として、UxU自身のテストがソースツリー内にありますUxUのSubversionリポジトリ全体をチェックアウトした上で tests/uxu/ 内にあるテストを実行してみてください。

単純なテスト

UxUでは、以下の要領で1ファイルにつき1つのテストケースを定義します。

var description = 'このテストケースの説明';

function setUp() {
  // 初期化処理
  // (インスタンスの生成など、各テストを実行する前に必ず実行する内容)
}

function tearDown() {
  // 終了処理
  // (インスタンスの破棄など、各テストを実行する前に必ず実行する内容)
}

function warmUp()
{
  // 前初期化処理
  // (クラス定義の読み込みなど、テストケース全体の最初に実行する処理)
}

function coolDown()
{
  // 後終了処理
  // (テストケース全体の最後に実行する処理)
}

testWillSuccess.description = '成功するテストの例';
testWillSuccess.priority    = 'normal';
function testWillSuccess() {
  assert.equals(0, [].length);
  assert.notEquals(10, ''.length);
  assert.isTrue(true);
  assert.isFalse(false);
  assert.isDefined(assert);
  assert.isUndefined(void(0));
  assert.isNull(null);
  assert.raises('TypeError', (function() { null.property = true; }), this);
  assert.matches(/patterns?/, 'pattern');
}

testWillFail.description = '失敗するテストの例';
testWillFail.priority    = 'low';
function testWillFail() {
  assert.isTrue(false);
}

グローバルな名前空間において以下の表にある名前で始まる名前の関数は、テストケースの内容として自動的に認識されます。また、プロパティで明示的に指定を行うことで、これら以外の名前の関数をテスト用関数として認識させることも可能です。

テスト用関数として自動的に認識される関数の名前
関数名(前方一致)テスト関数としての働きプロパティでの指定
setUp 初期化処理 function.isSetUp = true;
tearDown 終了処理 function.isTearDown = true;
warmUp 前初期化処理 function.isWarmUp = true;
coolDown 後終了処理 function.isCoolDown = true;
test 個々のテスト関数 function.isTest = true;

これらのテスト用関数として認識された関数は、以下の順番で実行されます。

  1. グローバルな名前空間に直接書かれたコード
  2. warmUp(前初期化処理)(あれば)
  3. setUp(初期化処理)(あれば)
  4. 1個目のテスト関数
  5. tearDown(終了処理)(あれば)
  6. setUp(あれば)
  7. 2個目のテスト関数
  8. tearDown(あれば)
  9. …中略…
  10. setUp(あれば)
  11. N個目のテスト関数
  12. tearDown(あれば)
  13. coolDown(後終了処理)(あれば)

ただし、個々のテスト関数の実行順序は保証されません。個々のテストはテストケース内でテスト関数を定義した順番と同じ順番で実行されるかもしれませんし、バラバラの順番で実行されるかもしれません。

テストケースそのものの名前、説明など補足的な情報を提供する場合は、グローバルな名前空間においてdescriptionという名前の変数に文字列で説明文を格納してください。テスト実行時などに自動的に利用されます。

MozUnit(MozLab)互換の記法

UxUは以下のようなスタイルで書かれたMozUnit(MozLab)用のテストケースも利用できます。この記法を使う場合、一つのファイルの中に複数のテストケースを含めることができます。

var unitTest = new TestCase('テストケースの説明');
unitTest.tests = {
  setUp : function() {
    // 各テストを実行する前に必ず実行する内容
  },

  tearDown : function() {
    // 各テストを実行した後に必ず実行する内容
  },

  '成功するテストの例': function() {
    assert.equals(0, [].length);
    assert.notEquals(10, ''.length);
    assert.isTrue(true);
    assert.isFalse(false);
    assert.isDefined(assert);
    assert.isUndefined(void(0));
    assert.isNull(null);
    assert.raises('TypeError', (function() { null.property = true; }), this);
    assert.matches(/patterns?/, 'pattern');
  },

  '失敗するテストの例': function() {
    assert.isTrue(false);
  }
};

UxUとMozUnitの相違点として、UxUではrunStrategyオプションは廃止されています。これは、後述する処理待ち機能によって、同期テストと非同期テストを定義時点で区別する必要がないためです。(ただし後方互換性のため、setUpが引数を受け取るようになっている場合は、MozUnit用の非同期テストと同様に振る舞うようになります。)

処理待ちを使ったテスト

UxU用のテストケースでは、yield式によって、テストの任意の位置で処理待ちを行うことができます。

テストケースや初期化処理の中にyield式を書くと、UxUはその行で処理を一時停止し、一定の条件が満たされた後に次の行から処理を再開します。処理を再開する条件はyield式に渡す値の内容によって変化します。詳細は以下の表の通りです。

yeild式に渡す値の内容と処理の再開条件の一覧
yield式に渡す値説明と例
数値

渡された数値をミリ秒(1000ミリ秒=1秒)単位のウェイト指定として解釈し、指定時間後に処理を再開します。

var beforeTime = (new Date()).getTime();
yield 1000; // 1秒待ってから処理を再開
var afterTime = (new Date()).getTime();
assert.isTrue((afterTime - beforeTime) > 500);
assert.isTrue((afterTime - beforeTime) < 1500);
valueプロパティを持つオブジェクト

渡されたオブジェクトのvalueプロパティの値が偽(false)である間は処理を一時停止し、値が真(true)になった時点で処理を再開します。いつまで待っても処理が再開されない(valueプロパティの値が真にならない)場合は、30秒でタイムアウトします。

var browser;
functionalTest.tests = {
  setUp : function() {
    var loaded = { value : false };
    browser = window.openDialog(
            'chrome://browser/content/browser.xul');
    browser.addEventListener('load', function() {
        loaded.value = true;
    }, false);
    // フラグが立つまで待ってから処理を再開
    yield loaded;
    browser.gFindBar.openFindBar();
  },
  tearDown : function() {
    browser.close();
  },
  ...
};
関数(※)

渡された関数を実行した返り値が偽(false)である間は処理を一時停止し、返り値が真(true)になった時点で処理を再開します。いつまで待っても処理が再開されない(返り値が真にならない)場合は、30秒でタイムアウトします。

var win = utils.getTestWindw();
var url = win.content.location.href;
// ブラウザで表示しているページが他のページに切り替わる
// (ページを遷移する)まで待ってから処理を再開
yield (function() {
        return url != win.content.location.href
    });
assert.equals(uri_to_be_redirected,
              win.content.location.href);
ジェネレータイテレータ(※)

渡されたジェネレータイテレータについて、自動的にイテレーションを行い、その間処理を一時停止します。イテレーションが終了した段階(StopIteration例外が発生した時点)で、次の行から処理を再開します。

// 一連の処理の完了を待って次に進む
function assert_page_load(aURI) {
  yield utils.loadURI(aURI);
  var win = utils.getTestWindow();
  assert.equals(aURI, win.content.location.href);
}

function assert_window_close() {
  var win = utils.getTestWindow();
  win.close();
  yield 500;
  assert.isTrue(win.closed);
}

yield assert_page_load('http://www.clear-code.com/');
yield assert_window_close();
ジェネレータ関数(※) 渡されたジェネレータ関数の返り値として取得されるジェネレータイテレータを使用し、yield式にジェネレータイテレータを渡した場合と同様に処理します。
function assert_window_close() {
  ...
}

yield assert_window_close; // without "()"
上記以外 上記以外のオブジェクトや値をyield式に渡したり、何も値を渡さないと、実行時にエラーとなります。

※関数やジェネレータをyield式に渡す場合の注意事項

yield式は関数の実行ではないため、スタックを生成しません。よって、yield式に渡された関数やジェネレータ関数、ジェネレータイテレータの中で例外が発生した場合は、スタックトレースを辿ってもyield式が評価された箇所まで到達できず、例外の発生箇所の特定が非常に困難となります。

// 例:リダイレクトのテスト
function assertRedirect(aOriginalURI, aToBeRedirectedTo) {
  yield utils.loadURI(aOriginalURI);
  assert.equals(aToBeRedirectedTo, browser.currentURI.spec);
}
yield assertRedirect('http://myhost/page1',
                     'http://myhost/page1_redirected');
yield assertRedirect('http://myhost/page2',
                     'http://myhost/page2_redirected');
yield assertRedirect('http://myhost/page3',
                     'http://myhost/page3_redirected');

例えばこのようなテストにおいては、assert.equals()でのアサーションに失敗して例外が発生しても、スタックトレースはassert.equals()がある行までしか辿ることができず、3回あるアサーションのどれが失敗したのかはレポートからは分からないということになります。

このような場合のために、UxUは特殊なヘルパー関数であるDo()を提供します。Do()を使用すると、yield式の評価の際にスタックを強制的に生成させることができ、例外の発生箇所の特定が容易になります。

yield Do(assertRedirect('http://myhost/page1',
                     'http://myhost/page1_redirected'));
yield Do(assertRedirect('http://myhost/page2',
                     'http://myhost/page2_redirected'));
yield Do(assertRedirect('http://myhost/page3',
                     'http://myhost/page3_redirected'));

なお、関数やジェネレータイテレータ以外のオブジェクトを渡した場合、Do()は渡されたオブジェクトをそのまま返しますので、テスト自体はDo()を使わない場合と同様に動作します。Do()を書き忘れるミスを防ぐためにも、テストケース中にyield式を書く際には、渡す値を必ずDo()でラップするようにしておくとよいでしょう。

テストの優先度

個々のテスト関数にはpriorityプロパティによって優先度を設定できます(MozUnit互換スタイルの記法でも利用できます)。以下は、テストに優先度を設定する場合の例です。

testBasic.priority = 'must';
function testBasic() {
  // とても重要な処理のテストは、必ず実施する。
  assert.equals(3, calcService.add(1, 2));
  ...
}

testMinor.priority = 'low';
function testMinor() {
  // あまり使われない機能のテストは、たまに実施する。
  ...
}

testUnderConstruction.priority = 'never';
function testUnderConstruction() {
  // 未実装の機能のテストは、実装を終えるまでは実施しない。
  // (毎回エラーが出ると、他のテストが成功したかどうかが
  //   分からなくなるので)
  ...
}

UxUは初期状態では、繰り返し何度もテストを実行するテスト駆動の開発スタイルを念頭に置いた設定になっており、テストの実行時には上記の優先度に従っていくつかのテストだけを実行します。ただし、前回実行時に失敗したテストと新しく追加されたテストは、優先度がneverである場合を除いて必ず実行されます。

これにより、一回一回のテストの実行にかかる時間を短縮すると同時に、失敗したテストについてコードを修正して再度テストを実行する中で他のテストも偏りなく実行されることが期待できます。

優先度は以下のいずれかを文字列で指定するか、0から1までの数値で指定します。

テストの優先度の一覧
優先度実行される確率数値での指定
must 100% 1
important 90% 0.9
high 70% 0.7
normal(初期値) 50% 0.5
low 25% 0.25
never 0% 0

テスト用プロファイルの利用

テストを一定の環境で実行することを強制するために、テストにはテスト実行用のプロファイルを指定することができます。

var profile = '../profiles/test-profile/';

function setUp() {
...

この例のように、プロファイルとして使用するフォルダへのパスをグローバルな名前空間の変数profileに格納しておくと、UxUは自動的にそのプロファイルでFirefoxやThunderbirdを起動してテストを実行します。

プロファイルとして指定したフォルダは、実際には、一時ファイルとして複製された物が利用されます。指定したフォルダそのものには変更は行われません。

var profile = '../profiles/test-profile/';
var application = 'C:\\Program Files\\Mozilla Thunderbird\\thunderbird.exe';

...

また、この例のようにアプリケーションの実行ファイルへのパスまたはファイルURLをグローバルな名前空間の変数applicationに格納しておくと、現在のアプリケーションではなくそのアプリケーションでテストを実行します。(※プロファイルが未指定の場合、applicationによる指定は無視されます)

テスト対象アプリケーションの指定

テストを実行する対象のアプリケーションを制限することができます。

var targetProduct = 'Thunderbird';

function setUp() {
...

この例のように、テスト対象アプリケーションの名前をグローバルな名前空間の変数targetProductに格納しておくと、UxUは現在のアプリケーションが指定された名前のアプリケーションである時だけテストを実行し、そうでない場合にはテスト全体を無視するようになります。FirefoxとThunderbirdの両方に対応したアドオンのテストを書くような場合に、Firefox依存の機能のテストがThunderbird上で実行されて無駄にエラーになったり、Thunderbird依存の機能のテストがFirefox上で実行されてエラーになったり、といった事態を防ぐことができます。

なお、テスト用プロファイルが同時に指定されている場合は、テスト全体を無視する代わりに、テスト対象のアプリケーションを指定プロファイルで自動的に起動してテストを実行します。(※Windows上でのみの機能)

アサーション

以下のアサーションを利用できます。

内容の比較

assert.equals(in Object aExpected, in Object aActual, [in String aMessage])
特定の値が期待される場面で使用します。期待通りの任意の値が渡されたかどうかを確認し、第2引数として与えられた値が第1引数として与えられた期待値と異なる場合はAssertionFailed例外を発生させます。値の比較はJavaScriptの==演算子と同じ精度で行われます。プリミティブ値、オブジェクト、DOMノード、配列の比較に対応しています。
assert.strictlyEquals(in Object aExpected, in Object aActual, [in String aMessage])
assert.equals()について、より厳しい基準で比較を行います。具体的には、値の比較はJavaScriptの===演算子と同じ精度で行われます。
assert.notEquals(in Object aExpected, in Object aActual, [in String aMessage])
特定の値が期待されない場面で使用します。任意の値と異なる値が期待通りに渡されたかどうかを確認し、第2引数の値が第1引数の値と等しい場合はAssertionFailed例外を発生させます。値の比較はJavaScriptの!=演算子と同じ精度で行われます。プリミティブ値、オブジェクト、DOMノード、配列の比較に対応しています。
assert.notStrictlyEquals(in Object aExpected, in Object aActual, [in String aMessage])
assert.notEquals()について、より厳しい基準で比較を行います。具体的には、値の比較はJavaScriptの!==演算子と同じ精度で行われます。
assert.contains(in Object aExpected, in Object aActual, [in String aMessage])
特定の値が含まれていることが期待される場面で使用します。第2引数として渡された値が配列である場合、第1引数で渡された内容が配列の要素に含まれていない場合はAssertionFailed例外を発生させます。第2引数の値がそれ以外の型である場合、文字列に変換した結果に第1引数で指定した文字列が含まれていない場合はAssertionFailed例外を発生させます。
assert.notContains(in Object aExpected, in Object aActual, [in String aMessage])
特定の値が含まれていないことが期待される場面で使用します。第2引数として渡された値が配列である場合、第1引数で渡された内容が配列の要素に含まれている場合はAssertionFailed例外を発生させます。第2引数の値がそれ以外の型である場合、文字列に変換した結果に第1引数で指定した文字列が含まれている場合はAssertionFailed例外を発生させます。
assert.isTrue(in aExpression, [in String aMessage])
値として真(または空でない文字列、0以外の数値など、真偽値として見たときに真となる値)が期待される場面で使用します。期待に反して偽が渡された場合はAssertionFailed例外を発生させます。
assert.isFalse(in aExpression, [in String aMessage])
値として偽(または空文字列、数値の0、null、undefinedなど、真偽値として見たときに偽となる値)が期待される場面で使用します。期待に反して真が渡された場合はAssertionFailed例外を発生させます。
assert.inDelta(in Number aExpected, in Number aActual, in Number aDelta, [in String aMessage])
値として一定の範囲内に収まる数値が期待される場面で使用します。第2引数の値が aExpected - aDelta から aExpected + aDelta の範囲外にある場合はAssertionFailed例外を発生させます。
assert.compare(in Number aExpected, in String aOperator, in Number aActual, [in String aMessage])
第1引数と第3引数を第2引数の演算子で比較します。期待に反して結果が偽の場合はAssertionFailed例外を発生させます。比較演算子以外を渡した場合はエラーになります。例:assert.compare(100, '<=', func());

assert.equals()assert.notEquals()assert.strictlyEquals()assert.notStrictlyEquals()assert.compare()に以下の型のオブジェクトを渡した場合は、特別なルールに則って比較が行われます。

Date
両者のオブジェクトが保持している時刻を比較します。
Array
配列の長さ、各要素を比較します。
Object型(オブジェクトリテラル、ハッシュ、カスタムクラスのインスタンス)
持っているすべてのプロパティの名前と値を比較します。

型の比較

assert.isBoolean(in aExpression, [in String aMessage])
値が真偽値であることが期待される場面で使用します。期待に反して真偽型でない値が渡された場合はAssertionFailed例外を発生させます。
assert.isNotBoolean(in aExpression, [in String aMessage])
値が真偽型でないことが期待される場面で使用します。渡された値が期待に反して真偽値である場合はAssertionFailed例外を発生させます。
assert.isString(in aExpression, [in String aMessage])
値が文字列であることが期待される場面で使用します。期待に反して文字列型でない値が渡された場合はAssertionFailed例外を発生させます。
assert.isNotString(in aExpression, [in String aMessage])
値が文字列型でないことが期待される場面で使用します。渡された値が期待に反して文字列である場合はAssertionFailed例外を発生させます。
assert.isNumber(in aExpression, [in String aMessage])
値が数値であることが期待される場面で使用します。期待に反して数値型でない値が渡された場合はAssertionFailed例外を発生させます。
assert.isNotNumber(in aExpression, [in String aMessage])
値が数値型でないことが期待される場面で使用します。渡された値が期待に反して数値である場合はAssertionFailed例外を発生させます。
assert.isFunction(in aExpression, [in String aMessage])
値が関数であることが期待される場面で使用します。期待に反して関数でない値が渡された場合はAssertionFailed例外を発生させます。
assert.isNotFunction(in aExpression, [in String aMessage])
値が関数でないことが期待される場面で使用します。渡された値が期待に反して関数である場合はAssertionFailed例外を発生させます。
assert.isDefined(in aExpression, [in String aMessage])
定義済み(数値、文字列、真偽値、nullなど)の値が期待される場面で使用します。渡された値が期待に反して未定義(undefined)である場合はAssertionFailed例外を発生させます。
assert.isUndefined(in aExpression, [in String aMessage])
未定義(undefined)の値が期待される場面で使用します。何らかの値が期待に反して渡された場合はAssertionFailed例外を発生させます。
assert.isNull(in aExpression, [in String aMessage])
nullが期待される場面で使用します。nullでない値(falseやundefinedなども含む)が期待に反して渡された場合はAssertionFailed例外を発生させます。
assert.isNotNull(in aExpression, [in String aMessage])
nullが期待されない場面で使用します。nullが期待に反して渡された場合はAssertionFailed例外を発生させます。

例外の発生と確認

assert.raises(in Exception aExpected, in Function aTestTask, in Object aScope, [in String aMessage])
特定の例外の発生が期待される場面で使用します。第2引数として渡された関数を、第3引数の値をスコープとして実行し、第1引数で指定された物と同じ例外が期待に反して発生しなかった場合はAssertionFailed例外を発生させます。
assert.notRaises(in Exception aExpected, in Function aTestTask, in Object aScope, [in String aMessage])
特定の例外の発生が期待されない場面で使用します。第2引数として渡された関数を、第3引数の値をスコープとして実行し、第1引数で指定された物と同じ例外が期待に反して発生した場合はAssertionFailed例外を発生させます。

所要時間の確認

assert.finishesWithin(in Number aExpectedTime, in Function aTestTask, in Object aScope, [in String aMessage])
assert.finishWithin(in Number aExpectedTime, in Function aTestTask, in Object aScope, [in String aMessage])
一定時間内に処理が完了することが期待される場面で使用します。第2引数として渡された関数を、第3引数の値をスコープとして実行し、処理を完了するまでに第1引数で指定された時間(単位:ミリ秒)以上の時間がかかった場合はAssertionFailed例外を発生させます。

正規表現によるマッチング

assert.matches(in RegExp aExpected, in String aActual, [in String aMessage])
特定のパターンの文字列が期待される場面で使用します。第2引数の値が第1引数の正規表現に期待に反してマッチしなかった場合はAssertionFailed例外を発生させます。
assert.notMatches(in RegExp aUnexpected, in String aActual, [in String aMessage])
特定のパターンでない文字列が期待される場面で使用します。第2引数の値が第1引数の正規表現に期待に反してマッチした場合はAssertionFailed例外を発生させます。
assert.pattern(in String aExpected, in RegExp aActual, [in String aMessage])
特定のパターンの正規表現が期待される場面で使用します。第2引数の値(正規表現)が第1引数として与えられた文字列に期待に反してマッチしない場合はAssertionFailed例外を発生させます。
assert.notPattern(in String aUnexpected, in RegExp aActual, [in String aMessage])
特定のパターンの正規表現が期待されない場面で使用します。第2引数の値(正規表現)がが第1引数として与えられた文字列にきた意に反してマッチする場合はAssertionFailed例外を発生させます。

アサーションのその他の機能

すべてのアサーションは、追加の引数として例外発生時に表示するメッセージを指定できます。スタックトレース以外で例外が発生した時の詳しい状況を知る必要がある場合などに利用できます。

また、以下のようにしてカスタムアサーションを定義することもできます。

assert.isOK = function(aActualValue) {
  var expected = 'OK';
  assert.equals(expected, aActualValue);
}

簡潔な記述が好みの方のために、すべてのアサーションはグローバル関数としても利用できます。グローバル関数として利用する場合の名前は以下のようになります(引数等の利用法は全く同一です): assertEqual(), assertNotEqual(), assertStrictlyEqual(), assertNotStrictlyEqual(), assertContain(), assertNotContain(), assertTrue(), assertFalse(), assertBoolean(), assertNotBoolean(), assertString(), assertNotString(), assertNumber(), assertNotNumber(), assertFunction(), assertNotFunction(), assertDefined(), assertUndefined(), assertNull(), assertNotNull(), assertRaise(), assertNotRaise(), assertMatch(), assertNotMatch(), assertPattern(), assertNotPattern(), assertInDelta(), assertCompare(), assertFinishWithin()


©2006-2009 ClearCode Inc.