先日SeleniumConf Tokyoに参加してきたのですが、実はその約1ヶ月前にもSeleniumConf Tokyo 2019 事前勉強会という会が開催されていたので参加してきました。
そこで最近CodeceptJSの記事を書かれている@tsuemuraさんの発表を聞いてCodeceptJSに興味を持ったので、自分なりに一通りの機能を試してみました。
使ってみた感想としては、CodeceptJSがベストな選択肢かどうかはまだ分からないですが、少なくとも2019年においてE2Eテスト目的であればベターな選択肢だと思います。PuppeteerやSeleniumを生で使うよりは圧倒的に便利でしょう。
CodeceptJSとは
まず最初にCodeceptJSの立ち位置を解説します。というのも、jsでブラウザを操作する類のツールは無数に存在しており自分がすぐに思いつくだけでもこれだけ存在します。
それぞれのツールによって裏側でSeleniumを使うのか使わないのかが異なっていたり、ブラウザ操作のためのAPIが異なっていたりと思想が異なっています。
そんな中でのCodeceptJSは、上で挙げたいくつかのツールのラッパーという立ち位置です。具体的にはWebdriverIO、Protractor、Nightmare、Puppeteerのラッパーです。
CodeceptJSは裏側のツールを抽象化したAPIを提供してくれます。つまり、CodeceptJSを使えば裏側のツールを切り替える必要が生じたときにそれぞれのAPIを覚え直す必要がなく、コードをそのまま使い回すことも可能です。
ここについての解説は自分が語るよりも公式のArchitectureの図を見てもらうと理解が早いでしょう。
さらにCodeceptJSは統一的なAPIだけではなく、E2Eテストで一般的に必要になる便利機能をたくさん用意してくれています。
単にChromeを操作するだけであればPuppeteerでも十分なのですが、PuppeteerをE2Eテストに利用しようと思うと誰もが必ず欲しいと思う機能がいくつか存在します。CodeceptJSを使うと車輪の再発明をせずにそれらの機能をすぐに手に入れることが可能になります。
それぞれの便利機能についてはqiita内で@tsuemuraさんを中心にすでに紹介されているので、この記事では全体を俯瞰する形で紹介していこうと思います。
以降の解説では、自分が今回作成したGitHubを対象としてE2Eテストを考えてみたサンプルコードから抜粋して紹介します。コード全体が気になる人はこちらも御覧ください。
前置き
CodeceptJSのコードを紹介する前にあらかじめお伝えしておきたいのですが、CodeceptJSのコードはjsっぽくないです。
自分はちょっとjs書ける人間ですが、正直に言ってしまうと最初は違和感だらけでした。特に最近はTypeScriptしか書いていなかったのでtsconfig.jsonが無いだけで不安でした。
ですが、見方を変えてCodeceptJSのテストコードはjsではなくて独自のDSLだと考えるとE2Eのテストコードとしては結構読みやすいと思うようになりました。それでいて、テストコードとは分離された設定ファイルは基本的にjsなので慣れ親しんだ書き方でプログラマブルに設定をすることも可能です。
エンジニアほどバリバリにjsを書けない人でもE2Eテストコードを読める/書けるフレームワークであることは、これもまた1つの価値ではないかと自分は思います。
どうかシンタックスがキモイというだけでスルーしないで最後までお楽しみいただけると嬉しいです。
await不要
簡単な例として、github.com/trending
のトレンドページにアクセスし、Trending
という文字が見えることを確認するテストを書いてみましょう。
まずは最近大人気なPuppeteerで実現してみます。
const puppeteer = require('puppeteer')
const assert = require('assert')
const main = async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('https://github.com/trending')
const text = await page.$eval('div.application-main > main > div > div > h1', el => {
return el.innerText
})
assert(text === 'Trending')
await browser.close()
}
main()
ブラウザを操作するほぼ全てのアクションにawait
が付いていますね。PuppeteerのAPIは基本的に非同期で戻り値がPromiseであり、thenのチェーンで繋ぐかあるいはawaitを使う必要があります。
これは2019年現在におけるjsでの非同期処理の基本中の基本ですが、jsに慣れていない人はハマりがちなポイントだと思います。jsに慣れている人でもE2Eのテストコードとして考えるといちいちawaitを付けるのは面倒くさいですし、うっかりawaitを付け忘れてしまうと意図しない動作をしてしまいます。
一方、同じテストをCodeceptJSで書くとこのようになります。
Feature('GitHub public')
Scenario('Trending', (I) => {
I.amOnPage('https://github.com/trending')
I.see('Trending')
})
非常にスッキリしていますね1。そしてawait
が存在していません。これでもブラウザを操作するコードの1行1行でちゃんと非同期処理を待ってくれます。
ただし、複雑なテストを書こうとした場合にはawait
が必要なこともあり、残念ながら完全に不要というわけではりません。
例えば、ブラウザの中のエレメントの値を読み取って変数として次のステップで使用する場合などです。どういうケースでawait
が必要になるのかは公式ドキュメントを見てみてください。
失敗時にスクリーンショット撮影
E2Eテストはテストが失敗したときのエラーログだけではデバッグが難しいため、失敗した状況のスクリーンショットはほぼ必須と言えるでしょう。
CodeceptJSは何も設定していなくてもテストが失敗したときに自動的にスクリーンショットを残してくれます。
厳密にはscreenshotOnFail
というプラグインのおかげなのですが、何も設定していなくてもデフォルトでオンになっています。
AutoLogin
E2Eテストにおいてログイン処理の共通化は誰もが通る道だと思います。ブラウザを立ち上げ直すたびにログイン処理を繰り返してもいいのですが、毎回ログイン処理を繰り返すのは時間的にちょっと無駄です。そしていたるところでログイン処理のコードを書くのはDRYではありませんね。
CodeceptJSにはログイン処理のためのAutoLogin
という強力なプラグインが存在します。AutoLoginの設定にログイン処理を書いておくことで全てのテストからログイン処理を簡単に行えるだけではなく、Cookieをファイルに保存しておくことで次回以降のログイン処理をスキップすることも可能にしています。
この挙動はプラグインの設定の以下の4つのステップを変更することで自由にカスタムすることも可能です。
-
login
: ログインするためのステップを記述する。一般的にはフォームにidとpasswordを入力してログインボタンを押すような処理 -
check
: ログイン済みかどうかをチェックするステップを記述する。一般的にはTOPページにアクセスしてログイン済みユーザだけに表示される要素が存在するかどうかをチェックします -
fetch
: ログイン後のCookieを保存する処理 -
restore
: ログイン前にCookieをセットする処理
ログイン処理とCookieの扱いがこの過不足のない4ステップに分解され、独自に上書き可能となっている点が非常によくできていると思います。
今回のサンプルコードでは以下のようにプラグインの設定をしています。
サンプルコードのcodecept.conf.jsよりAutoLoginの設定を抜粋。
autoLogin: {
enabled: true,
inject: 'login',
users: {
user: {
login: (I) => { // GitHubのログインフォームにUSERNAMEとPASSWORDを入力してSign inボタンを押す
const username = process.env.USERNAME
const password = process.env.PASSWORD
if (!username || !password) throw 'Env USERNAME or PASSWORD are null!!'
I.amOnPage('https://github.com/login')
I.fillField('Username or email address', username)
I.fillField('Password', password)
I.click('Sign in')
I.waitUrlEquals('https://github.com') // Sign inを押してから即移動しないため明示的にwait
},
check: (I) => { // ログイン済みの状態でTOPページにアクセスするとRecent activityという文字が見えるので、これでログイン済みかどうかを判定する
I.amOnPage('https://github.com/')
I.see('Recent activity')
},
restore: (I, cookies) => { // GitHubを開いてからCookieをファイルから読み込んでセットする
I.amOnPage('https://github.com/')
I.setCookie(cookies)
},
}
}
}
restore
はハマりがちなポイントで、自分もハマりました。@tsuemuraさんの記事が参考になると思います。
CodeceptJSのAutoLoginでログインが必要なテストを爆速にする
CustomStep
CodeceptJSが提供してくれる.amOnPage()
や.fillField()
などのステップを組み合わせて独自のカスタムステップを作成できます。
引数を取ることも可能なので、例えば特定のフォームの複数のinputに入力するような処理はカスタムステップとして定義しておくとテストコードをスッキリ書くことができるでしょう。
サンプルコードでは非常に簡単な例ですが、GitHubの自分のページにアクセスするという処理を繰り返し行っていたので、これをカスタムステップにしてみました。
// steps_file.js
module.exports = function() {
return actor({
// ここでのthisはテストコードでのIに置き換わる
amOnMyProfile: function() {
this.amOnPage('https://github.com/Kesin11')
}
})
}
このように定義しておくことでテストコードの中ではこのように使うことができます。
// I.amOnPage('https://github.com/Kesin11')と同じ動作をする
I.amOnMyProfile()
PageObject
E2Eのテストコードを分かりやすく、再利用しやすい形で書くためのデザインパターンとしてPageObjectパターンが知られています。
CodeceptJSはこのPageObjectパターンをフレームワークレベルでサポートしており簡単に実現できるようになっています。
// pages/top.js
const I = actor()
// PageObjectの定義
// 今回はGitHubのTOPページのPageObject
module.exports = {
fields: {
search_input: 'input.header-search-input'
},
search(string) {
I.click(this.fields.search_input)
I.fillField(this.fields.search_input, string)
I.pressKey('Enter')
}
}
// codecept.conf.jsより抜粋
// 定義したPageObjectをtopPageという名前で使えるようにする
include: {
I: './steps_file.js',
topPage: './pages/top.js'
}
// 実際に使用するテストコード
Scenario('Search me', (I, topPage) => {
I.amOnPage('https://github.com/')
topPage.search('Kesin11')
})
普通であればPageObjectとなるクラスやオブジェクトをテストコードでimport(またはrequire)する必要がありますが、CodeceptJSの場合はcodecept.conf.jsのinclude
に書くだけでScenario
の引数として渡されるので非常に簡単です。
ちなみに、この引数の名前はinclude
で設定した名前と一致していないと使うことができません。一般的なjsでは引数がオブジェクトでない限り、引数は名前ではなくて順番に依存したと思うのですが、どういう黒魔術なのだろうか・・・。
型定義ファイルの生成
ここまでjsを書いてきましたが、最近のTypeScript人気的にIDEが補完してくれない環境では、もはやコーディングが辛いという人もいるのではないかと思います。何を隠そう自分もその一人です。
CodeceptJSはこの問題を型定義ファイルを生成するというアプローチで解決してくれます。
npx codeceptjs def
を実行するとsteps.d.tsという型定義が生成されます。後はテストコードの冒頭に/// <reference path="./steps.d.ts" />
のように相対パスを記述するだけです2。
さらに素晴らしいのは、この生成された定義ファイルには先に紹介した自作のCustomStepやPageObjectの定義まで含まれているということです。すなわち、先ほど自作したI.amOnMyProfile
なども補完が可能になるということです!これが実に便利です。
Retry
E2Eテストにつきまとう問題として、どう頑張ってもテストが100%安定することはないという問題があります。
サイトのデザインが変更されたことでHTMLの構造が変化した場合に100%失敗するのは当然ですが、ネットワークの安定性や、アニメーションの微妙なラグなどによってHTMLの構造を全く変えていなくてもたまにテストが失敗するということがどうしても発生してしまいます。
テストコード側の工夫によってある程度対策は可能ですが、完璧に対策をせずともシンプルにリトライさせてしまうことでそれなりのケースを救うことができます。
CodeceptJSはリトライ処理も非常に簡単です。このようにFeatureにretry(1)
を付けるだけで、全てのシナリオに対して失敗した場合は1回までリトライをしてくれるようになります。1度失敗したテストもリトライして成功すればそれは成功とみなされます。
Feature('GitHub public').retry(1)
Scenario('Top page', (I) => {
I.see('Sign up')
})
Scenario('Trending', (I) => {
I.amOnPage('https://github.com/trending')
I.see('Trending')
})
今回の例ではFeatureレベルでのretryのサンプルですが、より細かいScenario、stepレベルでのリトライもサポートされています。
詳しくは公式ドキュメントのRetriesの項目を参照してください。
DataTable
E2Eテストを書いていると、ほぼ同じシナリオだけど例えばフォームに入力する値だけを変更したいというケースがあるかと思います。
このような場合によく行う方法としては、変数を配列か何かに格納してループを回すという方法です。いわゆるデータ駆動テストと呼ばれる方法です。
CodeceptJSはこのデータ駆動テストもサポートしています。こちらも実際のコードを見たほうが早いでしょう。
let repositories = new DataTable(['searchWord', 'orgReponame']) // 表のヘッダー行に相当する
repositories.add(['codeceptjs', 'Codeception/CodeceptJS'])
repositories.add(['selenium', 'SeleniumHQ/selenium'])
Feature('GitHub search using DataTable')
// currentは特別な変数で、repositoriesでループを回したときの現在の変数が入っている
Data(repositories).Scenario('Search repositories', (I, current, topPage) => {
I.amOnPage('https://github.com/')
topPage.search(current.searchWord) // Githubの検索フォームに入力してEnterを押すカスタムステップ
// 検索結果ページで一致するリポジトリが存在することをチェックし、クリックする
within('ul.repo-list', () => {
I.see(current.orgReponame)
I.click(current.orgReponame)
})
// 正しいリポジトリのページに飛べたことをチェック
I.seeCurrentUrlEquals(`https://github.com/${current.orgReponame}`)
})
これはGitHubのTOP上部の検索フォームにリポジトリ名を入力して検索し、正しいリポジトリに遷移できることを確認するテストです。
codeceptjs
とselenium
の2つをaddしていることから想像できるように、このコードだけでrepositoriesに含まれるデータ分だけテストを実行するデータ駆動テストを実現できます。
ちなみに、current
変数あたりの挙動が若干キモいと感じる方もいるかもしれませんが、正直言って自分も黒魔術感があって好きではありません・・・。
このエントリの前置きで述べたように、このあたりからjsというよりはDSLとして見てしまった方が悩まずに済むでしょう。
並列実行
ここまで、CodeceptJSを使うことでいかにE2Eテストが書きやすいかということを紹介してきましたが、E2Eテストには大きな弱点があります。それはテストに非常に時間がかかるということです。
テストコードが簡単に作れるからといってむやみにテストを作りすぎてしまうと、1回のテストが完走するのに1hかかる x ブラウザの種類 x 画面サイズ(PC, Mobile)
といった掛け算によってテストが全て完走するまでに半日かかってしまうということになりかねません。
テスト実行時間を減らす方法として思いつくのが、単純に複数のブラウザを同時に立ち上げてテストを実行させる並列化です。ブラウザを2つ起動して実行するだけで理論的には実行時間は1/2です。素晴らしいですね。
CodeceptJSはこの並列化もサポートしています。Puppeteerでブラウザを複数立ち上げる場合、設定に以下のように追加をします。
// codecept.conf.jsの一部を抜粋
multiple: {
parallel: {
chunks: 2
}
}
chunks: 2
はブラウザを2つ立ち上げ、テストファイル全体を2つに分割して並列に実行するという意味になります。
並列化を有効にするためには、通常の起動コマンドであるcodeceptjs run
の代わりにcodeceptjs run-multiple parallel
で起動する必要があります。
Seleniumを使用している場合は、テストファイルで分割して並列化せずにChromeやFirefoxなどブラウザの種類で並列に実行するという方法も可能です。詳しくは公式ドキュメントのAdvanced Usage#Parallel Executionを参照してください。
Allureレポートのサポート
個人的に一番アツい機能がこのAllureレポートの出力をサポートしていることです。Allureはあまりメジャーではないですが、OSSの優れたテストレポーターです。
Allureを使うとテストの実行結果だけではなく、スクリーンショットやテストケース毎の実行時間など様々な角度からテスト結果をHTMLで確認することが可能になります。
ユニットテスト程度であればそこまで高機能である必要はないですが、E2Eテストの場合は失敗したテストのスクリーンショットや時間がかかるテストの特定が非常に重要であるため、これらを把握しやすい優れたレポーターは地味に重要だったりします。
Allureレポートの設定を有効にするには以下のように設定します。これでAllureレポート生成に必要なファイルがoutputディレクトリに生成されるので、後はallure
のコマンドを使うことでレポートを生成できます。
// codecept.conf.js
plugins: {
allure: {
enabled: true
}
}
試しにいくつかのテストをわざと失敗するようにしてみました。このように過去のテスト結果の時間を比較したり、各テストを実行時間でソートすることも可能です。失敗したテストはログとスクショを確認することもできます。
Allure公式サイトでデモレポートを触ることも可能です。興味がある方はどのようなレポートが生成されるのかぜひ実際に確認してみてください。
Allureコマンド自体を今回は解説しませんが、サンプルコードのREADMEやpackage.jsonでAllureレポート生成について触れているので、そちらも参照してみてください。
まとめ
ここまで紹介してきたようにCodeceptJSにはE2Eテストで必要な機能がはじめから用意されています。逆に言えば、E2Eテストを本格的に始める場合にはブラウザを操作するコード以外にもこれぐらいの機能が必要ということでもあります。
E2Eテストを始めようと思ったとき、SeleniumやPuppeteerだけでゼロからE2Eテストを構築するよりもCodeceptJSを使うことで圧倒的に効率的、かつメンテしやすいテストコードを作成できるでしょう。
最後の余談にはなりますが自分が知る限りブラウザのE2Eテストの最先端では、いかに複数のブラウザでのテストを大規模に並列化させてテスト時間を短縮させるかというレベルに焦点が当てられています。
そのためにBrowserStackのようなSeleniumから操作できるブラウザを提供してくれるサービスを活用したり、SeleniumGridやさらにそれを発展させたツールによってdockerを活用した大規模なテスト並列化環境の構築などが行われています。
誰もが通るE2Eテスト共通の問題はCodeceptJSに任せて、より高レベルなE2Eテストを目指しませんか?