Sencha Test Futures APIについて
こんにちは、ゼノフィseoです。
インタラクティブなテストを書く上での一番の問題となってくるのは、非同期な性質です。Sencha Test はこういった非同期なテストを、同等な同期性なものと同じくらいに簡単にしてくれる新しい強力な API を提供しています。
Jasmine での非同期テスト
非同期なコードをテストするということは新しいことでもなんでもなく、Jasmine は初期状態から安定したサポートを提供してくれます。そして、テスト自体を非同期と自分でマークすることができます:
describe('Some tests', function () { it('takes time', function (done) { // << "done" marks test async doThings().then(done); }); }); |
ファンクションオブジェクト上に命名されたアーギュメントが存在し、it()
call にパスされているということは、Jasmine にとってテストが非同期性なものだということです。テストは(いずれ)Jasmine にテストが終わったことを “done” 関数をコールすることで伝えます。もし、テストが提供された done 関数を一定の時間(デフォルトでは5秒)コールしない場合は、テストは失敗扱いとされます。
この単純なアプローチは小さいテストでは難なく行えますが、もし複数のステップが含まれている場合、かかる時間を予想するのは難しいかもしれません。サーバーリクエストなどはテスト環境でも予想不可能だったりします。その環境で複数のブラウザやシナリオを同時にテストしている場合には、特にです。
done をコールするのは忘れがちではありますが、それは大体テストを書いている間に思い出せます。しかし、もしテストに複雑なロジックが含まれている場合、コードの枝によっては done をコールしたりしなかったりがある場合があります。
it('should eventually call done', function (done) { if (condition) { // true 98% of the time done(); } else { // oops - fails "randomly" } }); |
こういった枝分かれのロジックは、テストを書いている時はできる限り避けるべきです。
これらの全てのイシューは、インタラクティブテストの世界に入るまでは管理可能なものです。こういったテストはアプリケーション UI のエンドツーエンドテストやアプリケーションビューやコントローラのユニットテストで良く使われます。両方のケースに共通して、多くの手順には非同期なオペレーションとウェイトコンディション、そしてそれらが正当性チェックや期待値チェックなどと混ざったものが存在します。
Element Futures
インタラクティブなテストをより表現力豊か、そして管理しやすくするため、Sencha Test は ST.future.*
namespace 下でクラスファミリーを提供しています。それらは “futures” と呼ばれています。
Futures はテストコード内で作成されたオブジェクトであり、非同期なテストシーケンスを説明する簡潔なシンタックスを提供するものです。Futures の簡単な例ですが、以下のコードは ST.future.Element を使い、ST.element() ファクトリーメソッドによって返されます:
it('should change text on click', function () { ST.element('button#foo'). click(10, 10). textLike(/^Save/). and(function (el) { expect(el.hasCls('somecls')).toBe(true); }); }); |
ST.element()
メソッドはロケーター(XPath、Component Query、などを使用してエレメントをロケートするストリング)を認めています。返された ST.future.Element
インスタンスは上記で使用しているメソッドを提供しています:click()、 textLike()、と and() です。各メソッドは同じ ST.future.Element
を返します。
1つ頭に入れておかなければいけないのは、futures にされたコールは同期している様に見えますが、それらのオペレーションは瞬時に行われているわけではありません。その変わり、「実行可能時」に起きるアクションがスケジュール化されているのです。
ロケーター
我々のテストの最初の手順は ST.element()
を使って future エレメントインスタンスを作成します。これはこのメソッドの最初の手順ではありますが、それと同様に大事な仕事として、必要とされている DOM エレメントをロケートするスケジュールをリクエストするということです。水面下では、ST.element()
が提供されたロケーターをストアし、そのエレメントが DOM に加えられ、可視化されるのを待ち始めます。
エレメントをロケートし始めるタスクは、テストファンクションがブラウザにコントロールを返すまで始まりません。
アクション
テストの2つ目の手順としては、エレメント上にエレメントと関係した座標などを使って click()
をすることです。click()
メソッドがコールされると、スケジュールにクリックイベントが追加されます。多くの ST.future.Element
はアクションメソッドであり、同様に動作します:1つ前にスケジュールされたアクションに続くようにアクションをスケジュールし、future インスタンスによってロケートされたエレメント上で行います。
アクションメソッドは名前に動詞が使われています(例えば “click” など)。
ステート
テスト内の3つ目のステップは textLike()
メソッドです。これはこれはエレメントの text Content
が与えられたレギュラーエクスプレッションとマッチするまでウェイトをスケジュールします。このメソッドのグループはステートの状態を説明するためにあり、そのステートが達成されるまでスケジュールに遅延を挿入します。いくつかのステートメソッドはステートトランジションを探知するためにポーリングが必要ですが、その他はイベントをリッスンすることで変化を探知することができます。どの様な場合でも、この最適化ディテールは Sencha Test がハンドルするものであり、テストを書く人は気にする必要はありません。
ステートメソッドは名刺や説明文が名前に使われます(例えば“collapsed” や “textLike”)。
Inspections
テストの最後のピースはand()
メソッドへのコールです。このメソッドは1つ前のステップが終了すると与えられたファンクションをコールする様にスケジュールされます。この場合、textContent
がレギュラーエクスプレッションとマッチした後にです。and()
にパスされたファンクションは2つのアーギュメントまで受け取ることができます。この場合、1つしか宣言していません:ロケートされた St.Element
です。
選択可能な2つ目のアーギュメントは done
ファンクションであり、非同期 Jasmine テストと同じ様に動作します。もし、ファンクションが2つ目のアーギュメントを宣言した場合、done
ファンクションはパスされ、コールされなければなりません。
これらのファンクションは一般的にはエレメント、現在のステート、その他アプリケーションのアスペクトなどを検査するために存在しています。
カスタムウエイト
ウエイト状態がコードで表現されなければいけない場合があります。その場合、and()
ファンクションを改変し、テストが進行するように done ファンクションを受け入れるようにすることが可能です。
it('should change text on click', function () { ST.element('button#foo'). click(10, 10). textLike(/^Save/). and(function (el, done) { // wait for condition and call done() }); }); |
また、他の状況では、テストは適切なステートのためにポーリングしなければなりません。Futures は、これをハンドルするために wait()
メソッドを提供しています:
it('should change text on click', function () { ST.element('button#foo'). click(10, 10). textLike(/^Save/). wait(function (el) { return el.hasCls('somecls'); // return true-like when done }); }); |
一般的には、ポーリングを避ける and()
アプローチを使用するほうが良いのですが、実際の正しい選択はその状況によりけりとも言えます。
Component Futures
非同期的にエレメントとインタラクトすることは、ST.element()
を使うことで簡単になりました。しかし、Sencha Test Futures API は ST.component()
や、関連する futures でよりたくさんのことができるようになりました。これらのメソッドは ST.future.Component
から究極的に派生したクラスのインスタンスを作成します。これらのクラスは ST.future.Element
を拡張し、それらのコンポーネントタイプに適したアクションやステートメソッドを追加で提供します。
新しいテスト例について考えてみましょう:
it('should change cell text on click', function () { ST.grid('grid#foo'). row(42). cell('firstName'). reveal(). click(10, 10). textLike(/^Hello$/). and(function (cell) { expect(cell.el.hasCls('somecls')).toBe(true); }); }); |
ロケーターの増加
この例では、ST.grid()
後の最初の二つのコールはロケーターメソッドです:row()
と cell()
です。求められている行やセルを説明する方法はたくさんあります。これらはメソッド名が “row” と “cell” で始まるメソッドに対応しています。この場合、関連したレコード(42)の id
と カラム id (“firstName”) をとってくるメソッドを使用しています。
一度 row()
メソッドをコールすると、その row future 上の鎖状のメソッドコールが実行されます。以下のように行の grid()
メソッドをコールすることで、グリッドに登って戻ることができます。
ST.grid('grid#foo'). row(42). reveal(). // scroll the row into view click(10, 10). grid().row(999). // pick a different row reveal(). click(10, 10); |
これはセルにも同様に適応されます:
ST.grid('grid#foo'). row(42). cell('firstName'). // column id reveal(). // scroll cell into view click(10, 10). row(). // return to row 42 cell('lastName'). reveal(). click(10, 10). grid().row(999). reveal(). click(10, 10); |
アクションの増加
コンポーネント futures ヒエラルキー内のクラスは、いくつかの便利な Ext JS フレームワークメソッド用のアクションメソッドを提供します。例えば ST.future.Panel
は collapse()
と expand()
アクションメソッドを提供します。これらのアクションメソッドは適切なパネルメソッドに適切な時間にコールをスケジュールします。
ステートの増加
コンポーネント futures は追加のステートメソッドも提供します。例えば、ST.future.Panel
は collapsed()
と expanded()
ステートメソッドを提供し、パネルが求められたステートになるまでウエイトをスケジュールします。
継承ステート
ST.future.Component
は ST.future.Element
を拡張するため、それらのアクションとステートメソッドの多くを継承しています。この継承は futures クラスのヒエラルキーを続けて下っていきます。例えば ST.future.ComboBox
は ST.future.TextField
を拡張し、それは ST.future.Field
を拡張し、ST.future.Component
を拡張します。
Jasmine とのインタラクション
Sencha Test の Jasmine との統合は、futures が Jasmine の伝統的な非同期テストスタイルと一緒に動くようにデザインされています。その中でも、一般的には Jasmine の done ファンクションは futures を使用している時は使用する必要がありません(上記の例の通り)。
Future のオペレーションがいくつか完了すると、テストは完了し、Jasmine は次のテストを続けていきます。さらには、future シーケンスの各ステップは独自のタイムアウト値をコントロールできるため、全てのテストに対して1つ決める必要もありません。各 future アクションのタイムアウトは 5秒であるため、明白にタイムアウトを設定する必要もありません。
また、futures API のさらなる利点としては、and()
を使うことで非同期アクションとウエイト状態を同期性の検査と綺麗に混ぜることができるという点です。これにより。done ファンクションを全く使う必要がなくなり、テストの複雑性を最小限に抑えることができます。
DRYの原則
Futures はテストに DRY (Don’t Repeat Yourself / 同じことを2度するな) の原則を守らせることができます。必要なポイントで future インスタンスを作成するのではなく、以下の代替え案を検討してみてください。
describe('Many tests', function () { var Page = { fooGrid: function () { return ST.grid('grid#foo'); }, nameField: function () { return ST.textField('textfield[name="username"]'); } }; it('might take some time', function () { Page.fooGrid().row(42). reveal(). click(10, 10); }); it('might take some more time', function () { Page.fooGrid().row(999). reveal(). click(10, 10); }); }); |
上記の通り、我々は “Page” と呼ばれるオブジェクト内で集められたメソッドのセットを作成しています。このアプローチにより、テストはテスト対象(アプリケーション)のローケーターをカプセル化することができています。
もしページオブジェクトが複数のテストに使えるのであれば、describe()
ブロックの外に移動し、より適切な名前を与えることもできます。Sencha Test はシナリオ内の全ての JavaScript ファイルをロードするため、ページオブジェクトはシナリオ内の全てテストに対して有効となります。シナリオ間でページオブジェクトを共有する場合、それらはテストプロジェクトの Additional Libraries List に追加することができます。
結論
今回の記事で、Sencha Test Futures API がいかに、非同期性テストの複雑性を和らげるかを知ってもらえたらと思っています。ぜひ、一度は APIドキュメンテーションを確認していただき、提供されている全てのアクションとステートメソッドのリストを知ってください。 また、将来的な future のリリースでの、さらなる Ext JS コンポーネント への対応にも期待してください。