Spectronを使ってElectronアプリのテストを作っています。
試行錯誤でやっていますがいくつかノウハウが得られてきたので
Tipsというか忘れないようにメモしておきます。
async/awaitで書こう
GUIのような部分は基本的に非同期に振る舞うのでasync / awaitの記述が使えないとしんどいです。
Promiseを使って記述していっても良いですが
async / awaitの方がスッキリとした記述になって良いと思います。
テスト環境を選ぶ際はAsync function supportを謳っているAVAのようなテストフレームワークが良いと思います。私はAVAを使ってます。
AVAはデフォルトでテストを並列実行します。ユニットテストは良いかもしれませんがElectronだと上手くいかなかったりするので–serialオプションを付けて逐次実行するようにしています。
エレメントにIDを付けよう
テストではDOMの各エレメントにクリックしたり、値を入力したり、取得して結果を確認することになると思います。
エレメントにアクセスするには、各エレメントを特定する何らかの指定方法が必要です。一番簡単なのは、各エレメントにIDを振ることです。
例えば下記のような感じで、tdのファイル名を取得するために#filenameというIDを割り当てます。
この例は適当なので、もう少しちゃんとやる場合は、ネーミングルールを作り、画面ごとにユニークになるようにした方が良いと思います。例えば設定画面なら#settings-browsing-autoscrollとか機能や見た目のグループ単位でくくっていけばよいのではないでしょうか。自分はこの辺り、ちゃんとできていない(泣)
sample HTML
<tbody id="filelist">
<tr>
<td id="tick-file">
<input type="checkbox">
</td>
<td id="filename">000002.JPG</td>
<td>Sun Jan 08 2017 15:47:26 GMT+0900 (JST)</td>
<td>15634</td>
</tr>
<tr>
<td id="tick-file">
<input type="checkbox">
</td>
<td id="filename">001.JPEG</td>
<td>Sun Jul 01 2018 03:14:22 GMT+0900 (JST)</td>
<td>3080</td>
</tr>
<tr>
<td id="tick-file">
<input type="checkbox">
</td>
<td id="filename">001.JPG</td>
<td>Sat May 12 2018 00:32:23 GMT+0900 (JST)</td>
<td>3044</td>
</tr>
</tbody>
WebDriverIOを使ってアクセス
GUIを手動で操作する部分を自動化してテストすることになりますのでテストシナリオができたらテストを書いていきます。
GUIの各パーツは上記のエレメントIDで特定できますので、後はコードを書いていけばよいわけです。
各エレメントにアクセスする為の仕組みをSpectronは持っていて、内包しているWebDriverIOを通じて各エレメントにアクセスします。
WebDriverとは何か?というとブラウザを外部から操作するツールという認識で良いと思います。
WebDriverでググるとSeleniumというキーワードがたくさん出ます。SeleniumはWeb自動テスト界の重鎮みたいなものでNode.js向けに派生したものがWebDriverIO、という捉え方で良いと思います。なのでWebDriverIOでググると欲しい情報が出てくると思います。
Seleniumについては以前から知っていたのですが触る機会がなくて、今回環境は違えど遂に使うことができてオラわくわくすっぞ、という感じです。
WebDriverについてはこちらにまとまっているので参考になります。
WebDriverIOのAPIを見ながらさっそくテストを書いてみると
例えばこんな感じにインスタンスを立ち上げて
sample 01
import {Application} from 'spectron';
const electron = require('electron');
app = new Application({
path: electron,
args: [path.join(__dirname, '..')]
});
#aboutというIDを持ったエレメントをクリックしたい場合は
sample 02
const client = app.client
await client.$('#about').click()
のような書き方となります。
上記の$はelementの略として、$$はelementsの略となっています。
頻繁に使用するので省略記法というかjQuery的な書き方のほうがJavaScriptとしては馴染みがあるのでこっちのほうがわかりやすかもしれません。
引数はセレクターとなります。
書き方は2パターンあって下記はほぼ同じです。
sample 03
await client.$('#about').click()
await client.click('#about')
セレクターの指定・記述方法
エレメントのセレクターのフォーマットはCSSセレクターとXPathが使えます。
XPathやCSSセレクターについてはこちらがまとまっていますので参考になります。
それぞれのエレメントIDが完全にユニークになっていると楽なのですが、sampleのようにそうならない場合があります。複数の同じタイプのエレメントの中から選択する場合は、XPathの使用すると楽かもしれません。
これらをマニュアルで書くと面倒なのでXPathやCSSセレクターはツールで取得するのが吉です。
右クリックメニューから取得できるので、コピーして修正すれば使えると思います。
XPathやCSSセレクターで厳密に特定しない書き方もできます。例えば下記のような書き方になります。
sample 04
let tick_files = await client.$('#tick-file').$('input')
client.elementIdClick(tick_files[2].ELEMENT)
#tick-fileで複数エレメントを取得してその中のinputタイプの複数エレメントを取得、3つ目のinputをクリック、という流れになります。
elementIdClickは内部エレメントIDを使ってクリック、という動作になります。内部エレメントIDが何かと言うとtick_filesの中身を見るとわかります。
sample 05
[ { ELEMENT: '0.06472086141827971-8',
'element-6066-11e4-a52e-4f735466cecf': '0.06472086141827971-8',
value: { ELEMENT: '0.06472086141827971-8' },
selector: 'input',
index: 0 },
{ ELEMENT: '0.06472086141827971-9',
'element-6066-11e4-a52e-4f735466cecf': '0.06472086141827971-9',
value: { ELEMENT: '0.06472086141827971-9' },
selector: 'input',
index: 1 },
{ ELEMENT: '0.06472086141827971-10',
'element-6066-11e4-a52e-4f735466cecf': '0.06472086141827971-10',
value: { ELEMENT: '0.06472086141827971-10' },
selector: 'input',
index: 2 } ]
ELEMENTの値が内部エレメントIDになっています。内部エレメントIDは画面が書き換わるごとに更新されるようなので都度最新の内部エレメントIDを取得するようにしましょう。
また、この書き方を下記のように書きたいですが動作してくれません。Promiseを使えば上手くかけるかもしれませんが手間がかかりそうなので試していません。
sample 06
await client.$('#tick-file').$('input')[2].click()
この書き方は将来DOMツリーの構造が変わると動かなくなってしまうかもしれないのでもう少し工夫が必要かもしれません。あるいはDOMツリー構造までテストしているという捉え方で考えるべきか。DOMツリー構造に依存しないようにIDを振って行くように考えないといけないのか。悩みどころです。