E2Eテスト

もし、新しいAngularプロジェクトを始めるのであれば、近い将来現在のE2Eテストのメソッドが置き換わる可能性のある Protractorの使用を検討するのが良いかもしれません。

アプリケーションは成長していく度にサイズが増え複雑さを増すため、 手動になりがちな新しい機能の実装や調整の検証テストは非現実的になり、 バグやレベルダウンの原因になります。

この問題を解決するために、ユーザー操作をシミュレーションするAngularのシナリオランナーが用意されており、 Angularアプリケーションを検証する際に手助けしてくれます。

概要

特定された状態での与えられた確定操作において、アプリケーションがどのように振る舞うかを説明するJavaScriptのシナリオテストを書きます。 シナリオは、command(コマンド)とexpectation(期待)から作成される、 1つまたは複数のitブロック(アプリケーションでこれらを必要とすると考えられる事)から構成されます。 commandはRunnerにアプリケーションで行う内容を伝え(例:ページの移動やボタンのクリック)、 expectはRunnerに状態についての何らかのアサート(こうであるべきという主張)を伝えます(例:フィールドの値、現在URL等)。 もし、何らかのexpectationが失敗すると、Runnerは"failed"としてそれをマークして次に進みます。 シナリオはまた、成功失敗に関わらず各itブロックの前(または後)で実行される、 beforeEachafterEachを持つかもしれません。

Scenario runner

上記の要素に加えて、シナリオはitブロック内でのコードの重複を避けるためヘルパー関数を含めることも可能です。

下記はシンプルなシナリオの例です。

describe('Buzz Client', function() {
  it('should filter results', function() {
    input('user').enter('jacksparrow');
    element(':button').click();
    expect(repeater('ul li').count()).toEqual(10);
    input('filterText').enter('Bees');
    expect(repeater('ul li').count()).toEqual(1);
  });
});

input('user')は、 name="user"ではなく、ng-model="user"付きの<input>要素を見つけることに注意してください。

このシナリオは具体的に、"Buzz Client"ユーザーがフィルタリング手続き可能であるという要件を説明しています。 まずng-model="user"付きのinputフィールドへ値が入れられることで開始され、 ページ上の唯一のボタンがクリックされ、その後10項目のリストが存在していることを検証します。 次にng-model='filterText'付きのinputフィールドへ'Bees'と入力し、 リストが1つの項目に絞られることを検証します。

下記のAPIのセクションは、Runnerで利用可能なcommandとexpectationの一覧になります。

API

ソース: https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js

pause()
コンソール内でresume()が呼び出されるまでテストの実行を停止します。(またはRunnerUI内でresumeリンクをクリック)
sleep(seconds)
指定した秒数、テストの実行を停止します。
browser().navigateTo(url)
テストフレームにURLを読み込みます。
browser().navigateTo(url, fn)
テストフレーム内に関数によって返されるURLを読み込みます。 与えられたURLはテスト出力にのみ使用されます。 これはURLに直接遷移する際に使用されます。(テストを書く際に遷移先が不明)
browser().reload()
テストフレーム内に現在読み込んでいるページをリフレッシュします。
browser().window().href()
テストフレーム内に現在読み込んでいるページのwindow.location.hrefを返します。
browser().window().path()
テストフレーム内に現在読み込んでいるページのwindow.location.pathnameを返します。
browser().window().search()
テストフレーム内に現在読み込んでいるページのwindow.location.searchを返します。
browser().window().hash()
テストフレーム内に現在読み込んでいるページのwindow.location.hash(#無し)を返します。
browser().location().url()
テストフレーム内に現在読み込んでいるページの$location.url()を返します。
browser().location().path()
テストフレーム内に現在読み込んでいるページの$location.path()を返します。
browser().location().search()
テストフレーム内に現在読み込んでいるページの$location.search()を返します。
browser().location().hash()
テストフレーム内に現在読み込んでいるページの$location.hash()を返します。
expect(future).{matcher}
指定された見込み値が、マッチャー(matcher)を満たしていることを主張します。 全てのAPIのステートメントは、それらが実行された後に割り当てられた値を取得する、見込み(future)オブジェクトを返します。 マッチャーはangular.scenario.matcherを使用して定義され、 見込み値を使用してexpectationを実行します。 例:
expect(browser().location().href()).toEqual('http://www.google.com')
利用可能なマッチャーは、このドキュメントの下部に提示されています。
expect(future).not().{matcher}
指定された見込み値が、マッチャー(matcher)を満たさない事を主張します。
using(selector, label)
次のDSL要素選択のスコープです。
binding(name)
指定された名前の紐付けに、最初にマッチした値を返します。
input(name).enter(value)
ng-modelの名前に対応するテキストフィールド内に指定された値を入れます。
input(name).check()
ng-modelの名前に対応するチェックボックスのチェックを入れます/外します。
input(name).select(value)
ng-modelの名前に対応するラジオボタン内の指定された値のラジオボタンを選択します。
input(name).val()
ng-modelの名前に対応するinputフィールドの現在の値を返します。
repeater(selector, label).count()
指定されたjQueryセレクタがマッチする繰り返し処理内の行数を返します。 labelはテスト出力で使用されます。
repeater(selector, label).row(index)
指定されたjQueryセレクタがマッチする繰り返し処理内で、指定されたインデックスの行のbindingの配列を返します。 labelはテスト出力で使用されます。
repeater(selector, label).column(binding)
指定されたjQueryセレクタがマッチする繰り返し処理内で、指定されたbindingのカラム内の値の配列を返します。 labelはテスト出力で使用されます。
select(name).option(value)
指定されたng-model名のselectの指定された値のoptionを選択します。
select(name).options(value1, value2...)
指定されたng-model名の複数選択可能なselectの指定された値のoptionを選択します。
element(selector, label).count()
指定されたjQueryセレクタにマッチした要素の数を返します。 labelはテスト出力で使用されます。
element(selector, label).click()
指定されたjQueryセレクタにマッチする要素をくりっくします。 labelはテスト出力で使用されます。
element(selector, label).query(fn)
関数fn(selectedElements, done)を実行します。 selectedElementsは指定されたjQueryセレクタにマッチした要素で、 doneはfn関数の最後に呼び出される関数です。 labelはテスト出力で使用されます。
element(selector, label).{method}()

指定されたjQueryセレクタにマッチした要素上で呼び出したメソッド(method)の結果を返します。 メソッドには次のjQueryメソッドのいずれかを使用することが出来ます。

val、text、html、height、innerHeight、outerHeight、width、innerWidth、outerWidth、position、scrollLeft、scrollTop、offset

labelはテスト出力で使用されます。

element(selector, label).{method}(value)

指定されたjQueryセレクタにマッチした要素上でメソッド(method)に値(value)を渡して実行します。 メソッドには次のjQueryメソッドのいずれかを使用することが出来ます。

val、text、html、height、innerHeight、outerHeight、width、innerWidth、outerWidth、position、scrollLeft、scrollTop、offset

labelはテスト出力で使用されます。

element(selector, label).{method}(key)

指定されたjQueryセレクタにマッチした要素上で呼び出したメソッド(method)にキー(key)を渡した結果を返します。 メソッドには次のjQueryメソッドのいずれかを使用することが出来ます。

attr、prop、css

labelはテスト出力で使用されます。

element(selector, label).{method}(key, value)

指定されたjQueryセレクタにマッチした要素上でメソッド(method)にキー(key)と値(value)を渡して実行します。 メソッドには次のjQueryメソッドのいずれかを使用することが出来ます。

attr、prop、css

labelはテスト出力で使用されます。

マッチャー

マッチャー(Matcher)は、上述したexpect(...)関数と組み合わせて使用され、not()を付けて否定することが可能です。
例: expect(element('h1').text()).not().toEqual('Error')

ソースコード: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js

// 値とオブジェクトは、angular.equals()のルールに従って比較されます。
expect(value).toEqual(value)

// 単純な===を使用した値の比較です。
expect(value).toBe(value)

// 型をチェックすることで値が定義されているか確認します。
expect(value).toBeDefined()

// JavaScript標準のtrue/falseのルールを使用する2つのマッチャーです。
expect(value).toBeTruthy()
expect(value).toBeFalsy()

// 値(value)が指定された正規表現にマッチするか検証します。
// 正規表現は文字列または正規表現オブジェクトのどちらかを渡すことが出来ます。
expect(value).toMatch(expectedRegExp)

// null値であるかを===を使用して確認します。
expect(value).toBeNull()

// Array.indexOf(...)が使用され、要素が配列内に含まれているか否かを確認します。
expect(value).toContain(expected)

// < と >を使用して数値を比較します。
expect(value).toBeLessThan(expected)
expect(value).toBeGreaterThan(expected)

他の例も確認したいのであれば、angular-seedプロジェクトを参照してください。

element(...).query(fn)を使用した条件付きのアクション

AngularシナリオのE2Eテストは、キューのアクションによって高度な非同期と多くの複雑さを隠蔽し、 expectationは見込み処理を扱うことが出来ます。 時折、条件付きのアサーション、または要素選択が必要になる場合があります。 通常はこれの使用は避けるべきなのですが(不安定なテストになりがちであるため)、 条件の振る舞いをelement(...).query(fn)を使用して追加することが可能です。 下記のコードは、この関数がアプリケーションのWEBインターフェースを使用して追加された、 入力(入力は何らかのドメインオブジェクト)を削除するのに使用する方法についての一覧を示しています。

アプリケーションが下記の2つのビューから構成されているとします。

  1. 概観ビューはテーブルに全ての追加された入力の一覧を表示します。
  2. 詳細ビューは入力の詳細を表示し、削除ボタンが含まれています。 削除ボタンがクリックされると、ユーザーは概観ページへリダイレクトされて戻ります。
beforeEach(function () {
  var deleteEntry = function () {
    browser().navigateTo('/entries');

    // <tbody>要素を入力がなにも無いかもしれないケースを想定して
    // 選択する必要があります。(そのため行は無し)
    // セレクタがマッチ結果を得られなかった場合、テストは失敗がマークされます。
    element('table tbody').query(function (tbody, done) {

      // ngScenarioはjQuery liteでラップされた要素を与えてくれます。
      // `children()`関数を呼び出し、テーブルボディの行を取得します。
      var children = tbody.children();

      if (children.length > 0) {
        // もし少なくとも1つテーブルへ入力があれば、
        // リンクをクリックし、入力の詳細ビューへ。
        element('table tbody a').click();
        // そしてroute変更後、削除ボタンをクリックします。
        element('.btn-danger').click();
      }

      // もし、複数の入力がテーブル上にあれば、
      // 削除アクションを別にキュー登録します。
      if (children.length > 1) {
        deleteEntry();
      }

      // ngScenarioがテストの実行を続けられるように、
      // `done()`を呼び出すことを忘れないで下さい。
      done();
    });

  };

  // 入力の削除開始
  deleteEntry();
});

何が起こっているのを理解するために、ngScenarioが即座に実行されないことを重点に置く必要があったためキュー登録を行いました。 (ngScenario中、見込みアクションの追加について話しました) もし、テーブルに1つの入力しか無い場合、下記の見込みアクションはキュー登録されます。

// 入力1 削除
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();

2つ入力があれば、ngScenarioは下記のキューを動作させなければいけません。

// 入力1 削除
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();

    // 入力2 削除
    // "再起の深さ"を表すためのインデント
    browser().navigateTo('/entries');
    element('table tbody').query(function (tbody, done) { ... });
    element('table tbody a');
    element('.btn-danger').click();

注意事項

ngScenarioは、angular.bootstrapを使用した手動によるアプリケーションの起動では動作しません。 ng-appディレクティブを使用しなければいけません。

 Back to top

© 2017 Google
Licensed under the Creative Commons Attribution License 3.0.

このページは、ページトップのリンク先のAngularJS公式ドキュメント内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • AngularJSの更新頻度が高いため、元のコンテンツと比べてドキュメントの情報が古くなっている可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。