Siesta – No.4 操作シミュレーション
Siestaは、様々なユーザー操作をシミュレートすることができます。マウス、キーボード、そしてタッチ操作までも再現することができます。Siestaが、ただのユニットテスティングフレームワークでないことが、この機能1つ取り上げても、おわかりいただけるのでは無いでしょうか。
これらのUIテストを事前にいくつも用意しておいて、一気にいつでも確認できることは、品質が向上すること間違いありません。また、最近では、Sencha Architect 3のテストにもSiestaが利用されており、Senchaのテスト=Siestaのイメージが定着しつつあります。
Sencha Architectは、Windows、Linux、Macと各OS用のバイナリが提供されますが、中身はSencha Ext JSです、Sencha Ext JSで作成したものを、Desktop Packagerで、各OS用にバイナリを生成しています。そのため、元々はSencha Ext JSのコンポーネントですので、ブラウザ上でテストすることができるわけです。Sencha Architectレベルの複雑なアプリケーションも、こういったUIテスティングフレームワークを利用して、品質を維持する時代になりました。
今回は、マウス操作やキーボード操作など、その実際の操作方法を1つずつ紹介していきます。
マウス操作
マウス操作には、単純にクリックすることや、ダブルクリック、ドラッグなどの操作があります。その一つ一つみていきます。ここでは、一つ一つの動作を確認するための単純な記述方法を提示しますが、実際には連続する操作を簡潔に記述する方法もあります。それは、後ほど紹介します。
xy座標を指定して、クリックする
クリック操作をさせるには、clickメソッドを利用します。第1引数に、配列を指定して、1つ目にx座標を、2つ目にy座標を指定します。第2引数には、クリック後のコールバック関数を設定することができます。
1 2 3 | t.click([ 100, 100 ], function () { t.diag('x:100px, y:100px位置がクリックされました。'); }); |
CSSクラスを指定して、クリックする
指定したCSSクラスが設定されているエレメントに対して、クリックを行うことができます。つまり、CSSセレクタを利用して取得したエレメントに対してクリックをおこないます。また、クリックする位置は、取得したエレメントの中心位置をクリックします。
1 2 3 | t.click('.sample', function () { t.diag('.sample属性のタグがクリックされました。'); }); |
タグを指定して、クリックする
document.getElementsByTagNameのように、タグ名を指定してクリックします。クリック位置は、CSSクラスを指定した時と同様、選択されたエレメントの中央位置がクリックされます。
1 2 3 | t.click('input', function () { t.selectorExists('input:checked', 'チェックボックスにチェックが入っていること。'); }); |
ダブルクリックする
ダブルクリックさせるためには、doubleClickメソッドを利用します。座標、CSSセレクタ、タグ名で指定できるのは、clickメソッドと一緒です。 2回クリックされたことが、わかるように、次のHTMLを生成してから実行します。
1 2 3 4 5 6 7 8 9 | // テスト用のHTML生成 document.body.innerHTML = "".concat( '<div>', '<h1>ダブルクリックテスト</h1>', '<input type="button" class="sample" value="加算" onclick="document.getElementById('result').value++;;" />', '<br />', '<input type="text" id="result" value="0"/>', '</div>' ); |
引数の指定の仕方は、先ほど説明したとおり一緒ですので、メソッド名のみ変更すればOKです。
1 2 3 4 5 6 7 | t.doubleClick([ 100, 100 ], function () { t.diag('x:100px, y:100px位置がダブルクリックされました。'); }); t.doubleClick('.sample', function () { t.diag('.sample属性のタグがダブルクリックされました。'); }); |
テスト対象の入力フォーム画面の作成
次にテストするための簡単な入力フォーム画面を作成していきます。 No.1の工程でSencha Cmdを使ってプロジェクトを作成しましたが、プロジェクトルート直下のapp/viewディレクトリ配下にForm.jsを作成します。
app/view/Form.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | Ext.define('SiestaSample.view.Form', { extend: 'Ext.form.Panel', requires:[ 'Ext.layout.AnchorLayout' ], xtype: 'app-form', title: 'サンプル入力フォーム', bodyPadding: 5, layout: 'anchor', defaults: { anchor: '100%' }, defaultType: 'textfield', items: [{ xtype: 'fieldset', title: '基本情報', defaultType: 'textfield', layout: 'anchor', defaults: { anchor: '100%' }, items: [{ fieldLabel : '姓', name : 'first', allowBlank : false }, { fieldLabel : '名', name : 'last', allowBlank : false } ,{ fieldLabel : '電話番号', name : 'tel', allowBlank : false, emptyText : 'xxx-xxx-xxxx', maskRe : /[d-]/, regex : /^d{3}-d{3}-d{4}$/, regexText : '電話番号はxxx-xxx-xxxxの形式で入力して下さい' } ,{ fieldLabel : 'メールアドレス', name : 'mail', allowBlank : false, vtype : 'email' }] }, { xtype: 'fieldset', title: '付加情報', defaultType: 'textfield', layout: 'anchor', defaults: { anchor: '100%' }, items: [{ xtype: 'checkbox', name: 'input-check', boxLabel: '入力する場合はチェックを外して下さい', hideLabel: true, checked: true, margin: '0 0 10 0', scope: this, handler: function(box, checked){ var fieldset = box.ownerCt; Ext.Array.forEach(fieldset.query('textfield'), function(field) { field.setDisabled(checked); if (!Ext.isIE6) { field.el.animate({opacity: checked ? 0.3 : 1}); } }); } }, { fieldLabel : '住所1', name : 'address1', disabled: true }, { fieldLabel : '住所2', name : 'address2', disabled: true }] }], buttons: [{ text: 'リセット', name: 'reset', handler: function() { this.up('form').getForm().reset(); } }, { text: '登録', name: 'Submit', formBind: true, //only enabled once the form is valid disabled: true, handler: function() { var form = this.up('form').getForm(); if (form.isValid()) { form.submit({ success: function(form, action) { Ext.Msg.alert('Success', action.result.msg); }, failure: function(form, action) { Ext.Msg.alert('Failed', action.result.msg); } }); } } }] }); |
次にMainのコンテナに作成した入力フォームを組み込みます。プロジェクトルート直下のapp/view/Main.jsを下記のように修正します。
app/view/Main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Ext.define('SiestaSample.view.Main', { extend: 'Ext.container.Container', requires:[ 'Ext.tab.Panel', 'Ext.layout.container.Border', 'SiestaSample.view.Form' ], layout: { type: 'border' }, items: [{ region: 'west', xtype: 'panel', title: 'west', width: 300 },{ region: 'center', xtype: 'app-form' }] }); |
この時点で一度Webサーバ経由でプロジェクトルート直下のindex.htmlを表示してみて、作成した入力フォーム画面を確認して下さい。下記のような画面が表示されていればokです。
入力フォーム画面の仕様ですが、基本情報の4つのテキストフィールドが必須項目となっていて、これらに正しい値を入れると画面右下の登録ボタンが押下できるようになっています。付加情報は任意項目となっていて、チェックボックスのチェックを外すことで、2つのテキストフィールドに入力が出来るようになっています。右下のリセットボタンを押下すると、テキストフィールドに入力した値がクリアされます。
テストファイルの作成
フォーム画面をテストするためのファイルを作成します。今回テストするのは下記の3つです。
- 必須項目に対して正しい入力がされた時に登録ボタンが押下できるようになっていること。
- 付加情報のチェックボックスのチェックを外すと住所のテキストフィールドが入力可能になっていること。
- リセットボタンが押下されるとテキストフィールドの値がクリアされること。
まずは、プロジェクトルート直下のtests配下にgroup4というディレクトリを作成します。group4ディレクトリの中には、下記の3つのファイルを作成します。
- 401_form.t.js
- 402_form.t.js
- 403_form.t.js
401_form.t.jsを下記のように修正します。
tests/group4/401_form.t.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | StartTest(function(t) { var input = { first: 'ゼノフィ', last : 'xenophy', tel : '000-111-2222', mail : 'test@abc.com', address1: 'サンプル住所1', address2: 'サンプル住所2' } t.requireOk( [ 'SiestaSample.view.Form' ], function() { // form作って表示 var form = Ext.create('SiestaSample.view.Form',{ height: 600, width: 400, renderTo: Ext.getBody() }); t.diag('必須項目を入力して登録ボタンの押下'); // formのコンポーネント作成 var first = form.down('textfield[name=first]'), last = form.down('textfield[name=last]'), tel = form.down('textfield[name=tel]'), mail = form.down('textfield[name=mail]'), submit = form.down('button[name=Submit]'); t.chain({ action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action : 'type', target : first, text : input.first }, { action : 'wait', // or "delay" delay : 1000 // 1 second }, { // テキストフィールドに入力 action: 'type', target: last, text: input.last }, { action : 'wait', // or "delay" delay : 1000 // 1 second }, { // テキストフィールドに入力 action: 'type', target: tel, text: input.tel }, { action : 'wait', // or "delay" delay : 1000 // 1 second }, { // テキストフィールドに入力 action: 'type', target: mail, text: input.mail }, function(next){ t.notOk(submit.isDisabled(), '登録ボタンはdisabledではないです。'); }); } ); }); |
402_form.t.jsを下記のように修正します。
tests/group4/402_form.t.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | StartTest(function(t) { var input = { first: 'ゼノフィ', last : 'xenophy', tel : '000-111-2222', mail : 'test@abc.com', address1: 'サンプル住所1', address2: 'サンプル住所2' } t.requireOk( [ 'SiestaSample.view.Form' ], function() { // form作って表示 var form = Ext.create('SiestaSample.view.Form',{ height: 600, width: 400, renderTo: Ext.getBody() }); t.diag('付加情報のチェックボックスのチェックを外す'); // formのコンポーネント作成 var inputCheck = form.down('checkbox[name=input-check]'), address1 = form.down('textfield[name=address1]'), address2 = form.down('textfield[name=address2]'); t.chain({ action : 'wait', // or "delay" delay : 1000 // 1 second }, { // チェックボックスチェックをクリック action: 'click', target: inputCheck }, function(next){ t.notOk(address1.isDisabled(), '住所1はdisabledではないです。'); t.notOk(address2.isDisabled(), '住所2はdisabledではないです。'); }); } ); }); |
403_form.t.jsを下記のように修正します。
tests/group4/403_form.t.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | StartTest(function(t) { var input = { first: 'ゼノフィ', last : 'xenophy', tel : '000-111-2222', mail : 'test@abc.com', address1: 'サンプル住所1', address2: 'サンプル住所2' } t.requireOk( [ 'SiestaSample.view.Form' ], function() { // form作って表示 var form = Ext.create('SiestaSample.view.Form',{ height: 600, width: 400, renderTo: Ext.getBody() }); t.diag('全ての項目に入力してリセットボタンを押下'); // formのコンポーネント作成 var first = form.down('textfield[name=first]'), last = form.down('textfield[name=last]'), tel = form.down('textfield[name=tel]'), mail = form.down('textfield[name=mail]'), reset = form.down('button[name=reset]'), submit = form.down('button[name=Submit]'), inputCheck = form.down('checkbox[name=input-check]'), address1 = form.down('textfield[name=address1]'), address2 = form.down('textfield[name=address2]'); t.chain({ action : 'wait', // or "delay" delay : 1000 // 1 second }, { // テキストフィールドに入力 action : 'type', target : first, text : input.first }, { action : 'wait', // or "delay" delay : 1000 // 1 second }, { // テキストフィールドに入力 action: 'type', target: last, text: input.last }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action: 'type', target: tel, text: input.tel }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action: 'type', target: mail, text: input.mail }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // チェックボックスチェック action: 'click', target: inputCheck }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action: 'type', target: address1, text: input.address1 }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action: 'type', target: address2, text: input.address2 }, { action : 'wait', // or "delay" delay : 1000 // 3 second }, { // テキストフィールドに入力 action: 'click', target: reset }, function(next){ t.is(first.getValue(), '', '姓の値は空です。'); t.is(last.getValue(), '', '名の値は空です。'); t.is(tel.getValue(), '', '電話番号の値は空です。'); t.is(mail.getValue(), '', 'メールの値は空です。'); t.is(address1.getValue(), '', '住所1の値は空です。'); t.is(address2.getValue(), '', '住所2の値は空です。'); }); } ); }); |
作成した3つのテストファイルでは、テスト実行時にフォーム画面を生成して、フォーム画面内のコンポーネントに対してシュミレーション操作を実行しています。最後にアサーションを使って判定をするという流れです。実際に作成したテストファイルの中をみて確認して下さい。
テスト呼び出しファイルの修正
最後にプロジェクトルート直下の、harness.jsを修正します。No.1でテストケースのグループ化をやっているので、それに倣って下記のコードを追加します。
harness.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { group: 'グループ4', autoCheckGlobals : false, testClass: Siesta.Test.ExtJS, preload: [ "./ext/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css", "./ext/ext-all-debug.js" ], items: [ 'tests/group4/401_form.t.js', 'tests/group4/402_form.t.js', 'tests/group4/403_form.t.js' ] } |
また、同じくharness.jsですが、テスト全体の設定部分で名前空間とディレクトリを関連づけます。具体的には、下記のようにloaderPathを追記して下さい。
harness.js
1 2 3 4 5 6 7 8 | // コンフィグ指定 Harness.configure({ title : 'Siesta v2 サンプル for Sencha Ext JS', loaderPath : { 'SiestaSample' : 'app' }, preload : [ "./ext/ext-all-debug.js" ] }); |
テストの実行
Webサーバ経由でプロジェクトルート直下のharness.htmlにアクセスすると下記のようなテスト実行画面が表示されるので、グループ4のテストをクリックして実行してみて下さい。
まとめ
今回はシュミレーション動作が見て分かりやすくするために、テストケースの中にwaitを入れていますが、実際のテストケースではこれらのwaitは不要で一瞬で終わるように作成できます。このようなテストケースを追加してCI環境で回すことで、保守性の高い開発が実現できます。