Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What is going on with this article?
@takuya0301

Sencha Architect でコンポーネントを実装して Sencha Test で自動テストする

More than 1 year has passed since last update.

Sencha Architect でコンポーネントを実装して Sencha Test で自動テストするチュートリアルをメモっとく。

この記事では、下記のような本の一覧を管理するアプリケーションを Sencha Architect で実装して Sencha Test で自動テストしてみる。アプリケーションを構成するコンポーネントごとにテストを書いていくので、コンポーネントの独立性を感じられると思う。

Sample Application on Safari

前提条件

この記事では下記バージョンの 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

下記記事の手順でプロジェクトをすでに作成済みのものとする。

  1. Sencha Architect と Sencha Test で開発環境を構築する
  2. 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.Mainbooks.Toolbarbooks.Grid という3つのコンポーネントに分割している。

Component Division

ツールバー books.Toolbar を開発する

依存がなくシンプルなツールバー books.Toolbar から開発を始めていこう。

ツールバーの公開インターフェイスを設計する

ツールバーは追加ボタン (Add) と削除ボタン (Delete) があるだけのシンプルなツールバーだ。ただ、それだけだとチュートリアルとして物足りないので、削除ボタンの有効/無効を切り替えられるようにしてみる。グリッドで本が選択されているときだけ、削除ボタンをクリックできるようにするためだ。

それらを踏まえて公開インターフェイスとしてのコンフィグ・メソッド・イベントを設計すると下記のようになる。

books.Toolbar

Category Name Description
Config deleteButtonDisabled = false 削除ボタンの有効/無効の初期値
Method enableDeleteButton 削除ボタンを有効にする
disableDeleteButton 削除ボタンを無効にする
Event addButtonClicked 追加ボタンがクリックされた
deleteButtonClicked 削除ボタンがクリックされた

ツールバーを Sencha Architect で実装する

下記の手順でアプリケーションを実装する。

  1. ツールバーになる Ext.Toolbar を Views に追加する
  2. ツールバーに設定を追加する(userAlias のドットを削除するのを忘れるとテストで失敗するので注意すること)
    • userClassName: books.Toolbar
    • userAlias: bookstoolbar
  3. ツールバーにコンフィグを追加する
    • deleteButtonDisabled をコンフィグの検索フィールドに入力して Add ボタンをクリックする
    • カスタムコンフィグ左端のアイコンをクリックしてコンフィグのタイプを Boolean に設定する
    • デフォルト値として false を設定する
  4. ツールバーのコンフィグを ViewModel に反映させるために initialize メソッドを追加する(コンフィグパネルに initialize という項目がある)

    initialize: function() {
        this.callParent();
        this.getViewModel().set('deleteButtonDisabled', this.getDeleteButtonDisabled());
    }
    
  5. ツールバーの公開インターフェイスとなるメソッドとイベントを追加する(コンフィグパネルに 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');
    }
    
  6. 追加ボタンになる Ext.Buttonbooks.Toolbar に追加する

  7. 追加ボタンに設定を追加する

    • iconCls: x-fa fa-plus
    • text: Add
  8. 追加ボタンに tap イベントを捕捉するイベントバインディングを onAddButtonTap というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)

    onAddButtonTap: function(button, e, eOpts) {
        this.getView().addButtonClicked();
    }
    
  9. 削除ボタンになる Ext.Buttonbooks.Toolbar に追加する

  10. 削除ボタンに設定を追加する

    • iconCls: x-fa fa-minus
    • text: Delete
    • disabled: {deleteButtonDisabled} (data binding)
  11. 削除ボタンに tap イベントを捕捉するイベントバインディングを onDeleteButtonTap というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)

    onDeleteButtonTap: function(button, e, eOpts) {
        this.getView().deleteButtonClicked();
    }
    
  12. プロジェクトを保存する

ツールバーを Sencha Test でテストする

下記の手順でテストを実装する。

  1. Component シナリオの上位にある ExampleTest の Tests プロジェクトの設定を修正して保存する(ページオブジェクトの紐付けの関係)
  2. Component シナリオに Jasmine テストスイートを books/ToolbarTest.js という名前で追加する
  3. テスト実行時にコンポーネントを追加するために component 変数と beforeEach メソッドと afterEach メソッドを追加する

    let component;
    
    beforeEach(function () {
        component = Ext.create("Example.view.books.Toolbar");
        Ext.getApplication().viewport.add(component);
    });
    
    afterEach(function () {
        component.destroy();
    });
    
  4. Save ボタンをクリックして保存する

  5. デフォルトのテストケース内 expect(1).toBe(1); の末尾にカーソルを置いて Inspect ボタンをクリックする

  6. Inspect ダイアログで Chrome をクリックする

  7. Page Object のチェックボックスをチェックして BooksToolbar という名前を入力する

  8. Chrome ブラウザーに表示されたコンポーネントをクリックしてページオブジェクトに追加する

    • ツールバー本体
    • 追加ボタン
    • 削除ボタン
  9. 下記のように設定して Save Page Object をクリックして保存する

    Component Future Type Locator Name
    bookstoolbar component bookstoolbar booksToolbar
    button button button[text="Add"] addButton
    button button button[text="Delete"] deleteButton
  10. Exit ボタンをクリックして終了する

  11. 生成したページオブジェクトを使用するテストを books/ToolbarTest.js に追記する

    books/ToolbarTest.js
    describe("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();
            });
        });
    });
    
  12. Component シナリオで Chrome ブラウザーにチェックを入れ Run ボタンをクリックしてテストを実行する

  13. テストがすべて成功したときは Coverage ボタンをクリックしてテストカバレッジが100%になっていることを確認する

グリッド books.Grid を開発する

さらに、REST API 通信を伴うグリッド books.Grid を開発しよう。

グリッドを設計する

グリッドは本の一覧を管理できるグリッドになる。Sencha Ext JS の一般的なグリッドに行編集プラグインを追加することで要件は満たせる。ただ、このアプリケーションを前提にしたコンポーネントなので、明確に許可されている操作を公開インターフェイスで示すことが望ましい。

それらを踏まえて公開インターフェイスとしてのコンフィグ・メソッド・イベントを設計すると下記のようになる。

books.Grid

Category Name Description
Method createNewBook 新しい本を作成する
deleteSelectedBook 選択された本を削除する
Event bookSelected 本が選択された
bookDeselected 本の選択が解除された

グリッドを実装する

下記の手順でアプリケーションを実装する。

  1. グリッドになる Ext.grid.Grid を Views に追加する
  2. グリッドに設定を追加する(userAlias のドットを削除するのを忘れるとテストで失敗するので注意すること)
    • userClassName: books.Grid
    • userAlias: booksgrid
  3. グリッドに行編集プラグインを追加する
  4. グリッドの ViewModel に本ストアになる Ext.data.Store を追加する
  5. 本ストアのモデルになる Ext.data.Model を Models に追加する
  6. 本モデルに設定を追加する
    • userClassName: Book
  7. 本モデルにプロキシになる Ext.data.proxy.Rest を追加する
    • url: /books
  8. 本モデルに id フィールドになる Ext.data.field.Number を追加する
    • name: id
  9. 本モデルに title フィールドになる Ext.data.field.String を追加する
    • name: title
  10. 本モデルに author フィールドになる Ext.data.field.String を追加する
    • name: author
  11. 本ストアに設定を追加する

    • 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'
          }
      ]
      
  12. グリッドにストアを設定する

    • store: {books} (data binding)
  13. グリッドの公開インターフェイスとなるメソッドとイベントを追加する(コンフィグパネルに 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');
    }
    
  14. グリッドに select イベントを捕捉するイベントバインディングを onGridSelect というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)

    onGridSelect: function(dataview, selected, eOpts) {
        this.getView().bookSelected();
    }
    
  15. グリッドに deselect イベントを捕捉するイベントバインディングを onGridDeselect というメソッド名で追加する

    onGridDeselect: function(dataview, selected, eOpts) {
        this.getView().bookDeselected();
    }
    
  16. グリッドに Id 列になる Ext.grid.column.Number を追加する

    • flex: auto
    • width: (none)
    • dataIndex: id
    • editable: false
    • text: #
    • format: 0
  17. グリッドに Title 列になる Ext.grid.column.Text を追加する

    • flex: 1
    • width: (none)
    • dataIndex: title
    • editor:
    {
        xtype: 'textfield',
        name: 'title'
    }
    
    • text: Title
  18. グリッドに Author 列になる Ext.grid.column.Text を追加する

    • flex: 1
    • width: (none)
    • dataIndex: author
    • editor:
    {
        xtype: 'textfield',
        name: 'author'
    }
    
    • text: Author
  19. グリッドの不要な列を削除する

  20. プロジェクトを保存する

グリッドをテストする

下記の手順でテストを実装する。

  1. Component シナリオの上位にある ExampleTest の Tests プロジェクトの設定を修正して保存する(ページオブジェクトの紐付けの関係)
  2. Component シナリオに Jasmine テストスイートを books/GridTest.js という名前で追加する
  3. テスト実行時にコンポーネントを追加するために 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();
    });
    
  4. 行編集プラグインが表示する編集パネルもページオブジェクトに含めたいのでデフォルトのテストケース内に処理を追記する

    it("should pass", function () {
        ST.grid('booksgrid')
            .and(grid => grid.createNewBook());
        expect(1).toBe(1);
    });
    
  5. Save ボタンをクリックして保存する

  6. デフォルトのテストケース内 expect(1).toBe(1); の末尾にカーソルを置いて Inspect ボタンをクリックする

  7. Inspect ダイアログで Chrome をクリックする

  8. Page Object のチェックボックスをチェックして BooksGrid という名前を入力する

  9. Chrome ブラウザーに表示されたコンポーネントをクリックしてページオブジェクトに追加する

    • グリッド本体
    • 書名テキストフィールド
    • 著者テキストフィールド
    • 適用ボタン
  10. 下記のように設定して 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
  11. Exit ボタンをクリックして終了する

  12. 生成したページオブジェクトを使用するテストを books/GridTest.js に追記する

    books/GridTest.js
    describe("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();
            });
        });
    });
    
  13. Component シナリオで Chrome ブラウザーにチェックを入れ Run ボタンをクリックしてテストを実行する

  14. テストがすべて成功したときは Coverage ボタンをクリックしてテストカバレッジが100%になっていることを確認する

メイン books.Main を開発する

ツールバーとグリッドをまとめるメインを開発しよう。

メインを設計する

メインはツールバーとグリッドを包含して接続するコンテナーになる。イベントとメソッドをつなぐ部分も簡潔だ。また、結果的に公開インターフェイスとしてのコンフィグ・メソッド・イベントはメインには存在しない。

メインを Sencha Architect で実装する

下記の手順でアプリケーションを実装する。

  1. メインになる Ext.Panel を Views に追加する
  2. メインに設定を追加する
    • initialView: true
    • userClassName: books.Main
    • userAlias: booksmain
  3. ツールバーになる books.Toolbarbooks.Main にドラッグアンドドロップしてメインにリンクする形で追加する
  4. リンクしたツールバーに設定を追加する
    • deleteButtonDisabled: true
    • reference: toolbar
  5. グリッドになる books.Gridbooks.Main にドラッグアンドドロップしてメインにリンクする形で追加する
  6. リンクしたグリッドに設定を追加する
    • reference: grid
  7. リンクしたツールバーに addButtonClicked イベントを捕捉するイベントバインディングを onToolbarAddButtonClicked というメソッド名で追加する(コンフィグパネルに View Controller Event Bindings という項目がある)

    onToolbarAddButtonClicked: function(toolbar) {
        this.lookup('grid').createNewBook();
    }
    
  8. リンクしたツールバーに deleteButtonClicked イベントを捕捉するイベントバインディングを onToolbarDeleteButtonClicked というメソッド名で追加する

    onToolbarDeleteButtonClicked: function(toolbar) {
        this.lookup('grid').deleteSelectedBook();
    }
    
  9. リンクしたグリッドに bookSelected イベントを捕捉するイベントバインディングを onGridBookSelected というメソッド名で追加する

    onGridBookSelected: function(grid) {
        this.lookup('toolbar').enableDeleteButton();
    }
    
  10. リンクしたグリッドに bookDeselected イベントを捕捉するイベントバインディングを onGridBookDeselected というメソッド名で追加する

    onGridBookDeselected: function(grid) {
        this.lookup('toolbar').disableDeleteButton();
    }
    
  11. プロジェクトを保存する

メインをテストする

メインは公開インターフェイスが存在しない。テストするとなるとツールバーとグリッドを接続している部分になるが、このチュートリアルでは自動テストしないこととする。起動して手動テストしてみてほしい。

もしその部分をテストしたい場合は、テストコードの中で親コンポーネントに対するイベントを発火し、子コンポーネントのメソッド呼び出しをスパイするのがいいはず。なお、現時点でこの方法を検証はしていない。


この記事では Sencha Architect でコンポーネントを実装して Sencha Test で自動テストするチュートリアルを説明した。今回は小規模なアプリケーションだが大規模なエンタープライズアプリケーションの開発もできる環境なので、興味のある人はトライアル版で試してみてほしい。

1
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
takuya0301
Mikatus 株式会社の VP Engineering 兼デザイングループグループリーダー。最近は Event Storming と Lagom に興味がある。
mikatus
税理士・会計事務所向けクラウドシステムの企画、開発、提供事業を行っております。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
1
Help us understand the problem. What is going on with this article?