メインコンテンツへスキップ

ベストプラクティス

このガイドは、パフォーマンスが高く、回復力のあるテストを作成するのに役立つ、当社のベストプラクティスを共有することを目的としています。

回復力のあるセレクターを使用する

DOM の変更に強いセレクターを使用すると、たとえばクラスが要素から削除された場合でも、テストの失敗が少なくなるか、またはまったく失敗しなくなります。

クラスは複数の要素に適用できるため、そのクラスを持つすべての要素を意図的に取得する場合を除き、可能な限り避ける必要があります。

// 👎
await $('.button')

これらのセレクターはすべて、単一の要素を返す必要があります。

// 👍
await $('aria/Submit')
await $('[test-id="submit-button"]')
await $('#submit-button')

注意: WebdriverIO がサポートする可能性のあるすべてのセレクターを確認するには、セレクターのページを参照してください。

要素クエリの数を制限する

$ または $$ コマンドを使用するたびに (これには、それらをチェーニングすることも含まれます)、WebdriverIO は DOM 内で要素を特定しようとします。これらのクエリはコストがかかるため、できるだけ制限するように努める必要があります。

3 つの要素をクエリします。

// 👎
await $('table').$('tr').$('td')

1 つの要素のみをクエリします。

// 👍
await $('table tr td')

チェーニングを使用する必要があるのは、さまざまなセレクターストラテジーを組み合わせる場合のみです。例では、要素のシャドウ DOM 内に入るためのストラテジーであるディープセレクターを使用しています。

// 👍
await $('custom-datepicker').$('#calendar').$('aria/Select')

リストから 1 つの要素を取得するのではなく、単一の要素を特定することを優先する

これを実行することが常に可能とは限りませんが、:nth-child のような CSS 疑似クラスを使用すると、親の子リスト内の要素のインデックスに基づいて要素を照合できます。

すべてのテーブル行をクエリします。

// 👎
await $$('table tr')[15]

単一のテーブル行をクエリします。

// 👍
await $('table tr:nth-child(15)')

組み込みのアサーションを使用する

結果が一致するのを自動的に待機しない手動アサーションを使用しないでください。これは、テストが不安定になる原因になります。

// 👎
expect(await button.isDisplayed()).toBe(true)

組み込みのアサーションを使用することで、WebdriverIO は自動的に実際の結果が予期される結果と一致するのを待機し、回復力のあるテストにつながります。これは、アサーションが合格するかタイムアウトするまで自動的に再試行することで実現します。

// 👍
await expect(button).toBeDisplayed()

遅延読み込みとプロミスチェーン

WebdriverIO は、要素を遅延読み込みできるため、クリーンなコードを作成するという点でいくつかの秘策を持っています。これにより、プロミスをチェーンでき、await の量を減らすことができます。また、要素を Element の代わりに ChainablePromiseElement として渡し、ページオブジェクトでより簡単に使用できるようにすることもできます。

では、いつ await を使用する必要があるのでしょうか?$ および $$ コマンドを除き、常に await を使用する必要があります。

// 👎
const div = await $('div')
const button = await div.$('button')
await button.click()
// or
await (await (await $('div')).$('button')).click()
// 👍
const button = $('div').$('button')
await button.click()
// or
await $('div').$('button').click()

コマンドとアサーションの過度な使用を避ける

expect.toBeDisplayed を使用すると、要素が存在するまで暗黙的に待機します。すでに同じことを行うアサーションがある場合、waitForXXX コマンドを使用する必要はありません。

// 👎
await button.waitForExist()
await expect(button).toBeDisplayed()

// 👎
await button.waitForDisplayed()
await expect(button).toBeDisplayed()

// 👍
await expect(button).toBeDisplayed()

要素が明示的に非表示 (たとえば、opacity: 0) になっている場合や、明示的に無効になっている場合 (たとえば、disabled 属性) を除き、要素を操作したり、そのテキストのようなものをアサートしたりする場合、要素が存在したり表示されたりするのを待つ必要はありません。その場合、要素が表示されるのを待つことは意味があります。

// 👎
await expect(button).toBeExisting()
await expect(button).toHaveText('Submit')

// 👎
await expect(button).toBeDisplayed()
await expect(button).toHaveText('Submit')

// 👎
await expect(button).toBeDisplayed()
await button.click()
// 👍
await button.click()

// 👍
await expect(button).toHaveText('Submit')

動的テスト

テストに動的なテストデータ (たとえば、秘密の認証情報) をハードコードするのではなく、環境変数を使用して保存します。このトピックの詳細については、パラメーター化されたテストのページを参照してください。

コードをリンティングする

eslint を使用してコードをリンティングすると、エラーを早期に検出できる可能性があり、リンティングルールを使用して、いくつかのベストプラクティスが常に適用されるようにします。

一時停止しない

一時停止コマンドを使用するのは魅力的かもしれませんが、これは回復力がないため、長期的にはテストが不安定になるだけなので、悪い考えです。

// 👎
await nameInput.setValue('Bob')
await browser.pause(200) // wait for submit button to enable
await submitFormButton.click()

// 👍
await nameInput.setValue('Bob')
await submitFormButton.waitForEnabled()
await submitFormButton.click()

非同期ループ

繰り返したい非同期コードがある場合、すべてのループがこれを実行できるわけではないことを知っておくことが重要です。たとえば、Array の forEach 関数は、MDNで読めるように、非同期コールバックを許可しません。

注意: この例 console.log(await $$('h1').map((h1) => h1.getText())) で示されているように、操作を同期する必要がない場合は、これらを使用できます。

以下に、これが何を意味するかのいくつかの例を示します。

非同期コールバックはサポートされていないため、以下は機能しません。

// 👎
const characters = 'this is some example text that should be put in order'
characters.forEach(async (character) => {
await browser.keys(character)
})

以下は機能します。

// 👍
const characters = 'this is some example text that should be put in order'
for (const character of characters) {
await browser.keys(character)
}

シンプルにする

ユーザーがテキストや値などのデータをマップしているのを見かけることがあります。これは多くの場合必要なく、多くの場合コードの臭いであり、これが当てはまる理由については、以下の例を確認してください。

// 👎 too complex, synchronous assertion, use the built-in assertions to prevent flaky tests
const headerText = ['Products', 'Prices']
const texts = await $$('th').map(e => e.getText());
expect(texts).toBe(headerText)

// 👎 too complex
const headerText = ['Products', 'Prices']
const columns = await $$('th');
await expect(columns).toBeElementsArrayOfSize(2);
for (let i = 0; i < columns.length; i++) {
await expect(columns[i]).toHaveText(headerText[i]);
}

// 👎 finds elements by their text but does not take into account the position of the elements
await expect($('th=Products')).toExist();
await expect($('th=Prices')).toExist();
// 👍 use unique identifiers (often used for custom elements)
await expect($('[data-testid="Products"]')).toHaveText('Products');
// 👍 accessibility names (often used for native html elements)
await expect($('aria/Product Prices')).toHaveText('Prices');

また、単純なことが複雑な解決策を持っていることもあります。

// 👎
class BadExample {
public async selectOptionByValue(value: string) {
await $('select').click();
await $$('option')
.map(async function (element) {
const hasValue = (await element.getValue()) === value;
if (hasValue) {
await $(element).click();
}
return hasValue;
});
}

public async selectOptionByText(text: string) {
await $('select').click();
await $$('option')
.map(async function (element) {
const hasText = (await element.getText()) === text;
if (hasText) {
await $(element).click();
}
return hasText;
});
}
}
// 👍
class BetterExample {
public async selectOptionByValue(value: string) {
await $('select').click();
await $(`option[value=${value}]`).click();
}

public async selectOptionByText(text: string) {
await $('select').click();
await $(`option=${text}]`).click();
}
}

コードを並列で実行する

一部のコードが実行される順序を気にしない場合は、Promise.all を使用して実行を高速化できます。

注意: これによりコードが読みにくくなるため、ページオブジェクトまたは関数を使用してこれを抽象化できますが、パフォーマンスの利点が読みやすさのコストに見合う価値があるかどうかも疑問視する必要があります。

// 👎
await name.setValue('Bob')
await email.setValue('bob@webdriver.io')
await age.setValue('50')
await submitFormButton.waitForEnabled()
await submitFormButton.click()

// 👍
await Promise.all([
name.setValue('Bob'),
email.setValue('bob@webdriver.io'),
age.setValue('50'),
])
await submitFormButton.waitForEnabled()
await submitFormButton.click()

抽象化すると、ロジックが submitWithDataOf というメソッドに配置され、データが Person クラスによって取得される以下のように見える可能性があります。

// 👍
await form.submitData(new Person('bob@webdriver.io'))

ようこそ!ご用件は何でしょうか?

WebdriverIO AI Copilot