同期処理から非同期処理へ
V8 の変更により、WebdriverIO チームは 2023 年 4 月までに同期コマンド実行を非推奨にすることを発表しました。チームは移行をできるだけ簡単にするために懸命に取り組んでいます。このガイドでは、テストスイートを同期処理から非同期処理にゆっくりと移行する方法について説明します。サンプルプロジェクトとして、Cucumber Boilerplateを使用していますが、アプローチは他のすべてのプロジェクトでも同じです。
JavaScriptのPromise
WebdriverIO で同期実行が一般的だった理由は、Promise を扱う複雑さを取り除いているからです。特に、この概念が存在しない他の言語から来た場合、最初は混乱する可能性があります。ただし、Promise は非同期コードを扱うための非常に強力なツールであり、今日の JavaScript では実際に簡単に扱うことができます。Promise を使用したことがない場合は、MDN リファレンスガイドを確認することをお勧めします。ここで説明するには範囲外です。
非同期移行
WebdriverIO テストランナーは、同じテストスイート内で非同期および同期実行を処理できます。これは、テストと PageObject を自分のペースで段階的に移行できることを意味します。たとえば、Cucumber Boilerplate では、プロジェクトにコピーするための多数のステップ定義が定義されています。一度に 1 つのステップ定義または 1 つのファイルを移行できます。
WebdriverIO は、同期コードをほぼ完全に自動的に非同期コードに変換できるcodemodを提供しています。ドキュメントに記載されているように最初に codemod を実行し、必要に応じて手動移行にこのガイドを使用してください。
多くの場合、必要なのは、WebdriverIO コマンドを呼び出す関数をasync
にし、すべてのコマンドの前にawait
を追加することだけです。ボイラープレートプロジェクトで変換する最初のファイルである clearInputField.ts
を見ると、次のように変換されます。
export default (selector: Selector) => {
$(selector).clearValue();
};
に
export default async (selector: Selector) => {
await $(selector).clearValue();
};
それだけです。すべてのリライト例を含む完全なコミットをここで確認できます。
コミット:
- すべてのステップ定義を変換 [af6625f]
この移行は、TypeScript を使用するかどうかに依存しません。TypeScript を使用する場合は、最終的に tsconfig.json
の types
プロパティを webdriverio/sync
から @wdio/globals/types
に変更してください。また、コンパイルターゲットが少なくとも ES2018
に設定されていることを確認してください。
特殊なケース
もちろん、もう少し注意が必要な特殊なケースが常にあります。
ForEach ループ
たとえば、要素を反復処理するための forEach
ループがある場合は、イテレーターコールバックが非同期で適切に処理されるようにする必要があります。例:
const elems = $$('div')
elems.forEach((elem) => {
elem.click()
})
forEach
に渡す関数は、イテレーター関数です。同期の世界では、続行する前にすべての要素をクリックします。これを非同期コードに変換する場合は、すべてのイテレーター関数が実行を終了するまで待つ必要があります。async
/await
を追加することにより、これらのイテレーター関数は解決する必要がある Promise を返します。これで、forEach
はイテレーター関数の結果、つまり待機する必要がある Promise を返さないため、要素を反復処理するには理想的ではなくなりました。したがって、forEach
をその Promise を返す map
に置き換える必要があります。map
や find
、every
、reduce
などの Array の他のすべてのイテレーターメソッドも、イテレーター関数内の Promise を尊重するように実装されているため、非同期コンテキストでの使用が簡略化されます。上記の例は、次のように変換されています。
const elems = await $$('div')
await elems.forEach((elem) => {
return elem.click()
})
たとえば、すべての <h3 />
要素を取得し、そのテキストコンテンツを取得するには、次を実行できます。
await browser.url('https://webdriverio.dokyumento.jp')
const h3Texts = await browser.$$('h3').map((img) => img.getText())
console.log(h3Texts);
/**
* returns:
* [
* 'Extendable',
* 'Compatible',
* 'Feature Rich',
* 'Who is using WebdriverIO?',
* 'Support for Modern Web and Mobile Frameworks',
* 'Google Lighthouse Integration',
* 'Watch Talks about WebdriverIO',
* 'Get Started With WebdriverIO within Minutes'
* ]
*/
これが複雑すぎる場合は、単純な for ループを使用することを検討できます。例:
const elems = await $$('div')
for (const elem of elems) {
await elem.click()
}
WebdriverIO アサーション
WebdriverIO アサーションヘルパー expect-webdriverio
を使用する場合は、すべての expect
呼び出しの前に await
を設定してください。例:
expect($('input')).toHaveAttributeContaining('class', 'form')
を次のように変換する必要があります。
await expect($('input')).toHaveAttributeContaining('class', 'form')
同期PageObjectメソッドと非同期テスト
テストスイートで同期的に PageObject を記述している場合、非同期テストでは使用できなくなります。同期テストと非同期テストの両方で PageObject メソッドを使用する必要がある場合は、メソッドを複製し、両方の環境で提供することをお勧めします。例:
class MyPageObject extends Page {
/**
* define elements
*/
get btnStart () { return $('button=Start') }
get loadedPage () { return $('#finish') }
someMethod () {
// sync code
}
someMethodAsync () {
// async version of MyPageObject.someMethod()
}
}
移行が完了したら、同期 PageObject メソッドを削除し、名前を整理できます。
PageObject メソッドの 2 つの異なるバージョンを維持したくない場合は、PageObject 全体を非同期に移行し、browser.call
を使用して同期環境でメソッドを実行することもできます。例:
// before:
// MyPageObject.someMethod()
// after:
browser.call(() => MyPageObject.someMethod())
call
コマンドは、次のコマンドに進む前に、非同期の someMethod
が解決されるようにします。
結論
結果のリライト PRでわかるように、このリライトの複雑さはかなり簡単です。一度に 1 つのステップ定義を書き直せることを忘れないでください。WebdriverIO は、単一のフレームワークで同期実行と非同期実行を完全に処理できます。