開発

SpectronのテストTips

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セレクターはツールで取得するのが吉です。
右クリックメニューから取得できるので、コピーして修正すれば使えると思います。

Electron
Firefox

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を振って行くように考えないといけないのか。悩みどころです。

管理人

Recent Posts

Serifのスプリングセール – アドオンが50%オフ

Affinity Photoなどレタッチ…

3週間 ago

音声がロボットのようになるときの対処

リモート会議などでたまに相手の音声がおか…

2か月 ago

Serifのブラックフライデー – 全品40%オフ V1ユーザは更にお得!

恒例のブラックフライデーセールが始まりま…

4か月 ago

[rust] rayonで書き直してみました

前回のコードを元にrayonを使った処理…

5か月 ago

[rust] async-stdで書き直してみました

前回のコードをasync-stdで書き直…

5か月 ago

[rust] mutexを使わないバージョン tokio版ディレクトリ再帰的取得

前回の続き。mutexを使わないバージョ…

5か月 ago