Sencha Architect でコンポーネントを実装して Sencha Test で自動テストするチュートリアルをメモっとく。
この記事では、下記のような本の一覧を管理するアプリケーションを Sencha Architect で実装して Sencha Test で自動テストしてみる。アプリケーションを構成するコンポーネントごとにテストを書いていくので、コンポーネントの独立性を感じられると思う。
前提条件
この記事では下記バージョンの Sencha ツール群を使用している。
Tool | Version |
---|---|
Sencha Architect | 4.2.7 |
Sencha Ext JS | 7.1.0.48 |
Sencha Cmd | 7.1.0.16 |
Sencha Test | 2.3.0.328 |
下記記事の手順でプロジェクトをすでに作成済みのものとする。
- Sencha Architect と Sencha Test で開発環境を構築する
- Sencha Architect と Sencha Test の開発環境に REST API モックサーバーを追加する
なお各記事で確認用に追加しているアプリケーションとテストのコードは下記のコマンドで Add REST API mock server の時点に戻している。
$ git log --oneline
2a481bd (HEAD -> master) Add REST API mock server
d67e403 Generate test project
e7fd912 Generate main project
e94e6a0 Generate workspace
$ git add .
$ git reset --hard HEAD
HEAD is now at 2a481bd Add REST API mock server
アプリケーションをコンポーネントに分割する
アプリケーションを適切にコンポーネント分割しよう。複雑な UI では、コンポーネント分割が困難な場合がある。その場合、ワイヤーフレームなどの UI 設計に立ち返ってみるとよりユーザーフレンドリーでコンポーネント分割しやすい UI 設計が見えてくることが多い。
今回のアプリケーションは、books.Main
、books.Toolbar
、books.Grid
という3つのコンポーネントに分割している。
ツールバー books.Toolbar を開発する
依存がなくシンプルなツールバー books.Toolbar
から開発を始めていこう。
ツールバーの公開インターフェイスを設計する
ツールバーは追加ボタン (Add) と削除ボタン (Delete) があるだけのシンプルなツールバーだ。ただ、それだけだとチュートリアルとして物足りないので、削除ボタンの有効/無効を切り替えられるようにしてみる。グリッドで本が選択されているときだけ、削除ボタンをクリックできるようにするためだ。
それらを踏まえて公開インターフェイスとしてのコンフィグ・メソッド・イベントを設計すると下記のようになる。
books.Toolbar
Category | Name | Description |
---|---|---|
Config | deleteButtonDisabled = false | 削除ボタンの有効/無効の初期値 |
Method | enableDeleteButton | 削除ボタンを有効にする |
disableDeleteButton | 削除ボタンを無効にする | |
Event | addButtonClicked | 追加ボタンがクリックされた |
deleteButtonClicked | 削除ボタンがクリックされた |
ツールバーを Sencha Architect で実装する
下記の手順でアプリケーションを実装する。
-
ツールバーになる
Ext.Toolbar
を Views に追加する -
ツールバーに設定を追加する(
userAlias
のドットを削除するのを忘れるとテストで失敗するので注意すること)- userClassName: books.Toolbar
- userAlias: bookstoolbar
-
ツールバーにコンフィグを追加する
- deleteButtonDisabled をコンフィグの検索フィールドに入力して Add ボタンをクリックする
- カスタムコンフィグ左端のアイコンをクリックしてコンフィグのタイプを Boolean に設定する
- デフォルト値として false を設定する
-
ツールバーのコンフィグを ViewModel に反映させるために
initialize
メソッドを追加する(コンフィグパネルに initialize という項目がある)initialize: function() { this.callParent(); this.getViewModel().set('deleteButtonDisabled', this.getDeleteButtonDisabled()); }
-
ツールバーの公開インターフェイスとなるメソッドとイベントを追加する(コンフィグパネルに Functions という項目がある)
/* Methods */ enableDeleteButton: function() { this.getViewModel().set('deleteButtonDisabled', false); }, disableDeleteButton: function() { this.getViewModel().set('deleteButtonDisabled', true); }, /* Events */ addButtonClicked: function() { this.fireEvent('addButtonClicked'); }, deleteButtonClicked: function() { this.fireEvent('deleteButtonClicked'); }
-
追加ボタンになる
Ext.Button
をbooks.Toolbar
に追加する -
追加ボタンに設定を追加する
- iconCls: x-fa fa-plus
- text: Add
-
追加ボタンに
tap
イベントを捕捉するイベントバインディングをonAddButtonTap
というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)onAddButtonTap: function(button, e, eOpts) { this.getView().addButtonClicked(); }
-
削除ボタンになる
Ext.Button
をbooks.Toolbar
に追加する -
削除ボタンに設定を追加する
- iconCls: x-fa fa-minus
- text: Delete
- disabled: {deleteButtonDisabled} (data binding)
-
削除ボタンに
tap
イベントを捕捉するイベントバインディングをonDeleteButtonTap
というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)onDeleteButtonTap: function(button, e, eOpts) { this.getView().deleteButtonClicked(); }
-
プロジェクトを保存する
ツールバーを Sencha Test でテストする
下記の手順でテストを実装する。
-
Component シナリオの上位にある ExampleTest の Tests プロジェクトの設定を修正して保存する(ページオブジェクトの紐付けの関係)
- Location (URL): http://localhost:1841/test/#books.Toolbar
-
Component シナリオに Jasmine テストスイートを books/ToolbarTest.js という名前で追加する
-
テスト実行時にコンポーネントを追加するために
component
変数とbeforeEach
メソッドとafterEach
メソッドを追加するlet component; beforeEach(function () { component = Ext.create("Example.view.books.Toolbar"); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); });
-
Save ボタンをクリックして保存する
-
デフォルトのテストケース内
expect(1).toBe(1);
の末尾にカーソルを置いて Inspect ボタンをクリックする -
Inspect ダイアログで Chrome をクリックする
-
Page Object のチェックボックスをチェックして BooksToolbar という名前を入力する
-
Chrome ブラウザーに表示されたコンポーネントをクリックしてページオブジェクトに追加する
- ツールバー本体
- 追加ボタン
- 削除ボタン
-
下記のように設定して Save Page Object をクリックして保存する
Component Future Type Locator Name bookstoolbar component bookstoolbar booksToolbar button button button[text="Add"] addButton button button button[text="Delete"] deleteButton -
Exit ボタンをクリックして終了する
-
生成したページオブジェクトを使用するテストを books/ToolbarTest.js に追記する
books/ToolbarTest.jsdescribe("books.Toolbar", function () { let component; describe("when \"Delete\" button is enabled", function () { beforeEach(function () { component = Ext.create("Example.view.books.Toolbar"); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); }); it("has disabled \"Delete\" button after calling \"disableDeleteButtton\" method", function () { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.disableDeleteButton()); stpo.BooksToolbar .deleteButton() .disabled(); }); it("receives \"addButtonClicked\" event after clicking \"Add\" button", function (done) { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.on("addButtonClicked", () => done())); stpo.BooksToolbar .addButton() .click(); }); it("receives \"deleteButtonClicked\" event after clicking \"Delete\" button", function (done) { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.on("deleteButtonClicked", () => done())); stpo.BooksToolbar .deleteButton() .click(); }); }); describe("when \"Delete\" button is disabled", function () { beforeEach(function () { component = Ext.create("Example.view.books.Toolbar", { deleteButtonDisabled: true }); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); }); it("has enabled \"Delete\" button after calling \"enableDeleteButtton\" method", function () { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.enableDeleteButton()); stpo.BooksToolbar .deleteButton() .enabled(); }); it("receives \"addButtonClicked\" event after clicking \"Add\" button", function (done) { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.on("addButtonClicked", () => done())); stpo.BooksToolbar .addButton() .click(); }); it("does not receives \"deleteButtonClicked\" event after clicking \"Delete\" button", function () { stpo.BooksToolbar .booksToolbar() .and(toolbar => toolbar.on("deleteButtonClicked", () => fail())); stpo.BooksToolbar .deleteButton() .click(); }); }); });
-
Component シナリオで Chrome ブラウザーにチェックを入れ Run ボタンをクリックしてテストを実行する
-
テストがすべて成功したときは Coverage ボタンをクリックしてテストカバレッジが100%になっていることを確認する
グリッド books.Grid を開発する
さらに、REST API 通信を伴うグリッド books.Grid
を開発しよう。
グリッドを設計する
グリッドは本の一覧を管理できるグリッドになる。Sencha Ext JS の一般的なグリッドに行編集プラグインを追加することで要件は満たせる。ただ、このアプリケーションを前提にしたコンポーネントなので、明確に許可されている操作を公開インターフェイスで示すことが望ましい。
それらを踏まえて公開インターフェイスとしてのコンフィグ・メソッド・イベントを設計すると下記のようになる。
books.Grid
Category | Name | Description |
---|---|---|
Method | createNewBook | 新しい本を作成する |
deleteSelectedBook | 選択された本を削除する | |
Event | bookSelected | 本が選択された |
bookDeselected | 本の選択が解除された |
グリッドを実装する
下記の手順でアプリケーションを実装する。
-
グリッドになる
Ext.grid.Grid
を Views に追加する -
グリッドに設定を追加する(
userAlias
のドットを削除するのを忘れるとテストで失敗するので注意すること)- userClassName: books.Grid
- userAlias: booksgrid
-
グリッドに行編集プラグインを追加する
-
グリッドの ViewModel に本ストアになる
Ext.data.Store
を追加する -
本ストアのモデルになる
Ext.data.Model
を Models に追加する -
本モデルに設定を追加する
- userClassName: Book
-
本モデルにプロキシになる
Ext.data.proxy.Rest
を追加する- url: /books
-
本モデルに
id
フィールドになるExt.data.field.Number
を追加する- name: id
-
本モデルに
title
フィールドになるExt.data.field.String
を追加する- name: title
-
本モデルに
author
フィールドになるExt.data.field.String
を追加する- name: author
-
本ストアに設定を追加する
-
name: books
-
autoLoad: true
-
autoSync: true
-
model: Example.model.Book
-
data:
[ { id: 1, title: 'Learning Ext JS - Fourth Edition', author: 'Carlos A. Mendez, Crysfel Villa, Armando Gonzalez' }, { id: 2, title: 'Ext JS Application Development Blueprints', author: 'Colin Ramsay' }, { id: 3, title: 'Ext JS Essentials', author: 'Stuart Ashworth, Andrew Duncan' }, { id: 4, title: 'Mastering Ext JS - Second Edition', author: 'Loiane Groner' } ]
-
-
グリッドにストアを設定する
- store: {books} (data binding)
-
グリッドの公開インターフェイスとなるメソッドとイベントを追加する(コンフィグパネルに Functions という項目がある)
/* Methods */ createNewBook: function() { var books = this.getViewModel().getStore('books'), book = new Example.model.Book(); books.insert(0, book); this.findPlugin('rowedit').startEdit(book); }, deleteSelectedBook: function() { var books = this.getViewModel().getStore('books'), book = this.getSelection(); books.remove(book); }, /* Events */ bookSelected: function() { this.fireEvent('bookSelected'); }, bookDeselected: function() { this.fireEvent('bookDeselected'); }
-
グリッドに
select
イベントを捕捉するイベントバインディングをonGridSelect
というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)onGridSelect: function(dataview, selected, eOpts) { this.getView().bookSelected(); }
-
グリッドに
deselect
イベントを捕捉するイベントバインディングをonGridDeselect
というメソッド名で追加するonGridDeselect: function(dataview, selected, eOpts) { this.getView().bookDeselected(); }
-
グリッドに Id 列になる
Ext.grid.column.Number
を追加する- flex: auto
- width: (none)
- dataIndex: id
- editable: false
- text: #
- format: 0
-
グリッドに Title 列になる
Ext.grid.column.Text
を追加する- flex: 1
- width: (none)
- dataIndex: title
- editor:
{ xtype: 'textfield', name: 'title' }
- text: Title
-
グリッドに Author 列になる
Ext.grid.column.Text
を追加する- flex: 1
- width: (none)
- dataIndex: author
- editor:
{ xtype: 'textfield', name: 'author' }
- text: Author
-
グリッドの不要な列を削除する
-
プロジェクトを保存する
グリッドをテストする
下記の手順でテストを実装する。
-
Component シナリオの上位にある ExampleTest の Tests プロジェクトの設定を修正して保存する(ページオブジェクトの紐付けの関係)
- Location (URL): http://localhost:1841/test/#books.Grid
-
Component シナリオに Jasmine テストスイートを books/GridTest.js という名前で追加する
-
テスト実行時にコンポーネントを追加するために
component
変数とbeforeEach
メソッドとafterEach
メソッドを追加するlet component; beforeEach(function () { Server.start(); component = Ext.create("Example.view.books.Grid"); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); Server.stop(); });
-
行編集プラグインが表示する編集パネルもページオブジェクトに含めたいのでデフォルトのテストケース内に処理を追記する
it("should pass", function () { ST.grid('booksgrid') .and(grid => grid.createNewBook()); expect(1).toBe(1); });
-
Save ボタンをクリックして保存する
-
デフォルトのテストケース内
expect(1).toBe(1);
の末尾にカーソルを置いて Inspect ボタンをクリックする -
Inspect ダイアログで Chrome をクリックする
-
Page Object のチェックボックスをチェックして BooksGrid という名前を入力する
-
Chrome ブラウザーに表示されたコンポーネントをクリックしてページオブジェクトに追加する
- グリッド本体
- 書名テキストフィールド
- 著者テキストフィールド
- 適用ボタン
-
下記のように設定して Save Page Object をクリックして保存する
Component Future Type Locator Name booksgrid grid booksgrid booksGrid textfield textField textfield[name="title"] titleTextField textfield textField textfield[name="author"] authorTextField button button button[itemId="ok"] okButton -
Exit ボタンをクリックして終了する
-
生成したページオブジェクトを使用するテストを books/GridTest.js に追記する
books/GridTest.jsdescribe("books.Grid", function () { let component; describe("when \"Books\" grid has no books", function () { beforeEach(function () { Server.start(); component = Ext.create('Example.view.books.Grid'); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); Server.stop(); }); it("has a book after calling \"createNewBook\" method and editing its book", function () { stpo.BooksGrid .booksGrid() .and(grid => grid.createNewBook()); stpo.BooksGrid .titleTextField() .setValue("Book 1"); stpo.BooksGrid .authorTextField() .setValue("Alice"); stpo.BooksGrid .okButton() .click(); stpo.BooksGrid .booksGrid() .rowAt(0) .getRecord() .and(row => { const book = row.data.record; expect(book.id).toBe(1); expect(book.title).toBe("Book 1"); expect(book.author).toBe("Alice"); }); }); }); describe("when \"Books\" grid has a book", function () { beforeEach(function () { Server.start({ books: [ { id: 1, title: "Book 1", author: "Alice" } ] }); component = Ext.create('Example.view.books.Grid'); Ext.getApplication().viewport.add(component); }); afterEach(function () { component.destroy(); Server.stop(); }); it("has no books after calling \"deleteSelectedBook\" method with selection", function () { stpo.BooksGrid .booksGrid() .rowAt(0) .select(); stpo.BooksGrid .booksGrid() .and(grid => grid.deleteSelectedBook()) .and(grid => { const books = grid.getStore(); expect(books.count()).toBe(0); }); }); it("remains a book after calling \"deleteSelectedBook\" method without selection", function () { stpo.BooksGrid .booksGrid() .and(grid => grid.deleteSelectedBook()) .and(grid => { const books = grid.getStore(); expect(books.count()).toBe(1); }); }); it("receives \"bookSelected\" event after selecting book", function (done) { stpo.BooksGrid .booksGrid() .and(grid => grid.on("bookSelected", () => done())); stpo.BooksGrid .booksGrid() .rowAt(0) .select(); }); it("receives \"bookDeselected\" event after deselecting book", function (done) { stpo.BooksGrid .booksGrid() .and(grid => grid.on("bookDeselected", () => done())); stpo.BooksGrid .booksGrid() .rowAt(0) .select() .deselect(); }); }); });
-
Component シナリオで Chrome ブラウザーにチェックを入れ Run ボタンをクリックしてテストを実行する
-
テストがすべて成功したときは Coverage ボタンをクリックしてテストカバレッジが100%になっていることを確認する
メイン books.Main を開発する
ツールバーとグリッドをまとめるメインを開発しよう。
メインを設計する
メインはツールバーとグリッドを包含して接続するコンテナーになる。イベントとメソッドをつなぐ部分も簡潔だ。また、結果的に公開インターフェイスとしてのコンフィグ・メソッド・イベントはメインには存在しない。
メインを Sencha Architect で実装する
下記の手順でアプリケーションを実装する。
-
メインになる
Ext.Panel
を Views に追加する -
メインに設定を追加する
- initialView: true
- userClassName: books.Main
- userAlias: booksmain
-
ツールバーになる
books.Toolbar
をbooks.Main
にドラッグアンドドロップしてメインにリンクする形で追加する -
リンクしたツールバーに設定を追加する
- deleteButtonDisabled: true
- reference: toolbar
-
グリッドになる
books.Grid
をbooks.Main
にドラッグアンドドロップしてメインにリンクする形で追加する -
リンクしたグリッドに設定を追加する
- reference: grid
-
リンクしたツールバーに
addButtonClicked
イベントを捕捉するイベントバインディングをonToolbarAddButtonClicked
というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)onToolbarAddButtonClicked: function(toolbar) { this.lookup('grid').createNewBook(); }
-
リンクしたツールバーに
deleteButtonClicked
イベントを捕捉するイベントバインディングをonToolbarDeleteButtonClicked
というメソッド名で追加するonToolbarDeleteButtonClicked: function(toolbar) { this.lookup('grid').deleteSelectedBook(); }
-
リンクしたグリッドに
bookSelected
イベントを捕捉するイベントバインディングをonGridBookSelected
というメソッド名で追加するonGridBookSelected: function(grid) { this.lookup('toolbar').enableDeleteButton(); }
-
リンクしたグリッドに
bookDeselected
イベントを捕捉するイベントバインディングをonGridBookDeselected
というメソッド名で追加するonGridBookDeselected: function(grid) { this.lookup('toolbar').disableDeleteButton(); }
-
プロジェクトを保存する
メインをテストする
メインは公開インターフェイスが存在しない。テストするとなるとツールバーとグリッドを接続している部分になるが、このチュートリアルでは自動テストしないこととする。起動して手動テストしてみてほしい。
もしその部分をテストしたい場合は、テストコードの中で親コンポーネントに対するイベントを発火し、子コンポーネントのメソッド呼び出しをスパイするのがいいはず。なお、現時点でこの方法を検証はしていない。
この記事では Sencha Architect でコンポーネントを実装して Sencha Test で自動テストするチュートリアルを説明した。今回は小規模なアプリケーションだが大規模なエンタープライズアプリケーションの開発もできる環境なので、興味のある人はトライアル版で試してみてほしい。