みなさん、E2Eテスト書いてますか?
昔はブラウザテスト自動化と言えばSelenium一択だった気がしますが、最近はPuppeteerやCypressなどなど、色々選択肢があり、良い時代になりましたね。
今日はお気に入りのE2Eテストフレームワーク TestCafe
について紹介してみたいと思います。
TestCafeとは
OSSのE2Eテストフレームワークです。
長らく商用オンリーでプロプライエタリだったのですが、2016年頃からコア部分をOSSに切り替えたようです。
JavaScript(ES6)で書く事ができ、TypeScriptとCoffeeScriptもサポートしています。
イケてるところ
コマンド一つでマルチブラウザテスト環境が構築できる
npm install -g testcafe
これだけで完成です。SeleniumのようにWebDriverのインストールは必要ありません。
あとはテストスクリプトを書いてお好きなブラウザで実行するだけ。
試しに、Googleで TestCafe
と入力して検索するだけのシンプルなスクリプトを書いてみましょう。
import { Selector } from 'testcafe'
fixture('Sample')
test('検索', async t => {
await t
.navigateTo('https://google.co.jp')
.typeText(Selector('input[type=text]'), 'TestCafe')
.pressKey('enter')
})
簡単に解説しておくと、
-
fixture
はSeleniumとかでいうところのテストスイートの宣言です。複数のテストケースを一つのfixture
で束ねる形になります。 -
test
はテストケースです。省略しましたが、本来はこの中にアサーションを書いたりします。 -
t
は実行時にtestcafeのコントローラーが渡されます。
これを各ブラウザで実行してみましょう。いわずもがな、ブラウザが既にインストールされてないと動かないです。
$ testcafe chrome test.js
$ testcafe firefox test.js
$ testcafe edge test.js
$ testcafe ie11 test.js
え?どうせなら全ブラウザで同時実行したい?欲張りですね!そんなあなたにもTestCafeは応えてくれます。
$ testcafe all test.js
たったこれだけで、ローカルにインストールされている全ブラウザでテストが同時実行されます。最高ですね!
公式ドキュメントによると、サポートしているブラウザは下記の通りです。(2018/12/1現在)
- Google Chrome: Stable, Beta, Dev and Canary
- Internet Explorer (11+)
- Microsoft Edge
- Mozilla Firefox
- Safari
- Android browser
- Safari mobile
実験してないですが、理論上は他のブラウザでも動くはず……!あくまで公式にサポートしてるのが↑ということで。
リモートブラウザでの実行ができる
「普段の開発はMacやLinuxでやってるけど、やっぱIEやEdgeでの動作確認はしたいよねー」という方は多いと思います。
そんなときは、お手元のコンソールにそっと testcafe remote
とタイプしてください。
$ testcafe remote test.js
お、なんかURLが表示されましたね!
同一ネットワーク上にいるPCならOSやブラウザを問わず、このURLに接続することで、テストが実行されます。
同様に、AndroidやiOSでのテストも可能ですが、スマホにこのURLをポチポチ入力するのはさすがに苦行なので、QRコードを表示させましょう。
testcafe remote --qr-code test.js
このQRCodeをスマホで撮影して、表示されたURLにアクセスすると、スマホのブラウザでテストが実行できます。
(念の為ですが、↑のQRコードはダミーですので、必ず手元のコンソールで表示させてくださいね)
PageObjectPatternとの親和性が高い
上述のテストコードでも出てきましたが、TestCafeではセレクタを Selector
というオブジェクトとして表現します。
(一応、 Selector
を使わず書くことも出来ますが、書いたほうがいいです)
Selector
は変数に代入できるので、例えばこんな感じでPageObjectを定義することができます。
import { Selector } from 'testcafe'
fixture('Sample')
// ページオブジェクト
const page = {
searchInput: Selector('input[type=text]')
}
test('検索出来る', async t => {
await t
.navigateTo('https://google.co.jp')
.typeText(page.searchInput, 'TestCafe')
.pressKey('enter')
})
同様のことはSeleniumやCypressでも出来ますが、DOM要素を返すgetterを定義する感じの実装になることが多いので、自分はTestCafeの Selector
を使った記法が割と気に入っています(好みの問題とは思いますが)。
要素が表示されるまでよしなに待ってくれる
さて、このSelector
オブジェクトですが、引数で渡したセレクタが画面上に表示されるまで良い感じに待ってくれるという特徴があります。
この「いい感じに待ってくれる」タイミングですが、公式ドキュメントによると下記2つのようです。
- 何らかのアクションの対象として
Selector
が渡されたとき -
Selector
が評価されたとき
前者は、例えばt.click()
の引数としてSelector
が渡された時。後者はSelector('input')()
のように記述した時です。
先のスクリプトを元に説明するとこんな感じ。
import { Selector } from 'testcafe'
fixture('Sample')
const page = {
searchInput: Selector('input[type=text]') // ここでは要素の表示をチェックしない
}
test('検索出来る', async t => {
await t
.navigateTo('https://google.co.jp')
.typeText(page.searchInput, 'TestCafe') // ここで要素の表示をチェックされる
.pressKey('enter')
})
もし、何らかの理由で、「何もアクションはしないけど要素の表示は待ちたい」という場合は
page.searchInput()
// もちろんこっちでも可
Selector('input[type=text]')()
のように、Selectorを評価するだけでその要素が表示されるのを待ってくれます。
ちなみに、「よしなに待ってくれる」と言いつつ、規定のタイムアウト時間を過ぎるとその時点でテストは失敗します。
もちろんデフォルトのタイムアウト時間は設定できますし(コマンドライン引数で設定)、セレクタ単位でも指定できます。
Selector('input[type=text]', {timeout: 30000}) // 30秒待って表示されなかったらテスト失敗
表示されている文字列で要素を指定できる
定番のCSSセレクタでの指定以外に、特定の文字列を含む要素
みたいな選択も出来ます。
例えば、こんな感じの入力フォームを考えてみましょう。
<form>
<div>
<label for="name">お名前</label>
<input name="name" type="text">
</div>
<div>
<label for="company">勤務先</label>
<input name="company" type="text">
</div>
<button type=submit>送信する</button>
</form>
送信ボタンを取得するとき、普通に Selector('button[type=submit]')
と書いても良いのですが、「送信する
という文字列を含むbutton要素」という指定もできます。
Selector('button').withText('送信する')
「お名前」というラベルの付いたテキストボックスを取得するには、こんな感じで書けます。
Selector('div').withText('お名前').find('input')
// 厳密にやるなら……
Selector('label').withText('お名前').sibling('input')
もちろん、素直に Selector('input[type=name]')
と書いても良いのですが、例えば何かのはずみで「お名前」というラベルが表示されなくなっていた、なんて時に検知できません。
ユーザーは当然ラベルを手がかりに入力しているので、この書き方の方がより堅牢なテストになるかなーと思ってます(ケースバイケースだとは思いますが……)。
ちなみに、Cypressでも同等の記述をサポートしています(cy.contain('送信')
。
あんまり詳しくないですが、セレクタにXPathを利用できるフレームワークならbutton[contains(text(), '送信')]
とかでも出来そう。
ログインが必要なテストが高速に実行できる
ログインが必要なアプリケーションでは、テストケース実行のたびにログイン処理を行っているとテスト実行時間が無駄に長くなってしまいます。
また、特に業務アプリの場合だと、ユーザーの権限によって画面が変わったり、権限の異なるユーザーで交互に操作をする(例えばワークフロー系のシステムで、社員が申請を出し、部長が承認する、的な)テストが必要になることがあり、その都度ログアウト〜ログインの処理を書くのは面倒くさいですね。
TestCafeにはUser Rolesという機能があり、ログイン直後の状態を保存しておき、テストケース間で使いまわしたり、テストケース中で切り替えたりすことができます。
以下はUser Rolesのサンプルです。このサンプルでは
- staff
- manager
という2つのRoleを定義し、
- staffは日報を書ける
- managerは日報を書けない
- staffが申請を出し、managerが承認できる
というテストケースを書いてみます。なお、テスト対象となるサンプルアプリはありません。心の眼で見て下さい。
import { Selector,Role } from 'testcafe';
const staff = Role('http://example.com/login', async t => {
await t
.typeText('#login', 'staff')
.typeText('#password', 'password')
.click('#sign-in');
});
const manager = Role('http://example.com/login', async t => {
await t
.typeText('#login', 'manager')
.typeText('#password', '1qaz2wsx')
.click('#sign-in');
});
fixture('User Role Sample')
test('staffは日報を書ける', async t => {
await t
.useRole(staff) // staffのログインスクリプトが実行される
.click(Selector('a').withText('日報'))
.typeText(Selector('input[placeholder=ここにタイトルを入力]'), '2018/12/2の日報')
.typeText(Selector('input[placeholder=ここに内容を入力]'), '今日も頑張った')
.click(Selector('button').withText('送信'))
.expect(Selector('td').withText('2018/12/2の日報').exists).ok()
})
test('managerは日報を書けない', async t => {
await t
.useRole(manager) // managerのログインスクリプトが実行される
.expect(Selector('a').exists).notOk()
})
test('staffが出した申請をmanagerが承認できる', async t => {
await t
.useRole(staff) // ログインスクリプトは実行されない
.click(Selector('a').withText('申請'))
.typeText(Selector('input[placeholder=ここに申請内容を入力]'), '5000兆円ください')
.click(Selector('button').withText('送信'))
.useRole(manager) // ログインスクリプトは実行されない
.click(Selector('a').withText('申請/承認'))
.click(Selector('td').withText('5000兆円ください').find('a'))
.click(Selector('button').withText('承認する'))
})
Roleの定義方法はシンプルで、Roleオブジェクトの引数としてログインスクリプトを渡すだけです。
定義したRoleをテストケース内で使うには、 t.useRole(Role)
というメソッドを使いますが、Roleに渡したログインスクリプトが実行されるのは最初の一回のみです。
そのため、 staffが出した申請をmanagerが承認できる
テストケースではログインスクリプトは一切実行されません。初回実行直後にCookie等が保存されているので、その状態をロードするだけです。テストケースの数が増えるほど、この仕組みにより享受できるメリットは大きくなります。
この辺は他のフレームワークに比べて非常に気が利いてると思っていて、例えばCypressではログイン時間短縮のためにログインフォームに直接HTTPリクエストを投げてUIを描画させないことでログイン時間を短縮するというアプローチを推奨しています(!)。
イケてないところ
動画やページ全体のスクリーンショットが撮れない
残念ながら、SeleniumやCypressのように動画を撮る機能がありません。ページ全体のスクリーンショットを取る機能もありません。
動画に関しては、一応Issueが存在するので、まあ来世に期待でしょうか……
Cypressのようなイカした開発環境がない
ここは本当にCypress羨ましいんですが……残念ながら、「ステップ毎のDOMNodeを保持して自由に行ったり来たりできる」みたいな素敵な開発環境はありません。
一応、実行時に --debug
オプションを付けることでステップ実行したり、ファイルの編集を検知して再実行してくれるtestcafe-liveというツールは存在します。
baseUrlの設定ができない
Cypressだと、cypress.json
にbaseUrlを指定すると、cy.visit()
でbaseUrl部分は省略することが出来るのですが、残念ながらTestCafeにはその辺の機能がありません……
自分はDotEnvで環境変数として指定し、↓のような感じで書いてますが、毎回これを書くのもダルいのでなんとかしたい
t.navigateTo(`{process.env.baseUrl}/foo/bar`)
ググラビリティが低い
TestCafeは当初はOSSではなく商用Webサービスとして展開していた為、 TestCafe Example
とかでググると商用サービスの方がヒットしてしまったりします。例えば https://testcafe.devexpress.com/Documentation/Examples/ など。
OSSとしてのTestCafeのドキュメントは https://devexpress.github.io/testcafe/ の方にまとまっているので、こちらを主に参照するようにすると幸せになれます。
おわりに
他のE2Eフレームワークに比べていまいち知名度の低いTestCafeですが、個人的には最高に使いやすいと思っているので、興味があればぜひ触ってみてください。
それでは、良いテストライフを!