Cypressのテストエラー XMLHttpRequest.xhr.onerror に嵌った件とおまけ

XMLHttpRequest.xhr.onerror に嵌った件

Cypress によるテストを定期的に実行していたところ、ある日突然 XMLHttpRequest.xhr.onerror によるエラーで停止するようになり、解決したので記録として残しておきます。

結論としては、chainableではない cy.log()cy.log().get().. とchainして使用していたのが原因でした。

状況

  • 問題のコードを含むテストをUbuntu上で実行した場合、エラーが高頻度で発生する
  • 問題のコードを含むテストをWindows上で実行した場合、エラーが発生しない
  • Cypressのテストで使用するブラウザに依存しない

上記の記述ミスによるエラーは、反映後しばらくしてから突如顕在化したもので、必ず発生するものではありません。これ以外の原因もあるかもしれませんが、ご参考になれば幸いです。

おまけ cy.wait() の使用は可能な限り避けよう

次のような、Aのボタンをクリック後にバックエンドで何らかの処理があり、処理終了後に出現するボタンBをクリックするテストケースがあるとします。

例えば、検索ボタンをクリックし、そのレスポンスを利用する場合を想定してください。なお、例では明示的なassertionを省略しています。

cy.get('[data-cy=buttonA]').click()  // ボタンAの要素を取得しクリック
cy.get('[data-cy=buttonB]').click()  // ボタンBの要素を取得しクリック

ボタンAをクリックした後のレスポンスに時間がかかる場合、ボタンBの要素が見つからずにタイムアウトエラーとなる場合があります。そんなときの対応方法です。

1. cy.get()のオプションで待ち時間を指定する

cy.get() の待ち時間を指定することで、デフォルトの待ち時間である 4000ms から変更することができます。

cy.get('[data-cy=buttonA]').click()
cy.get('[data-cy=buttonB]', {timeout: 7500}).click()

2. cy.waitUntil()を使用して特定条件まで待機する

cypress-wait-until というプラグインをインストールすることで、特定条件を満たすまで待機することができます。この方法については、@aomoriringo さんの Cypressで送る快適E2Eライフ が大変参考になりました。ありがとうございます。

処理後の文言変化を利用する

ボタンをクリックした後に「検索中」→「検索が完了しました」のような文言変化があることが明らかな場合、selector に指定した文言が出るまで検索を繰り返すという方法があります。

cy.waitUntil(() = {
  return cy.get('body').then(($body) = {
    const selector = ":contains('検索が完了しました。')"
    const has_loaded = $body.find(selector).length
    return has_loaded 0
  })
}), { timeout: 5000, interval: 200 }

過去には、selector = '[data-cy=buttonB]‘ のように指定することで、1.のタイムアウトオプションが動作しない場合の代替手段として有効でした。現在では1.が使用できるため、推奨しません。

Cookieの有無を利用する

特定処理をトリガーにCookieが発行される場合のサンプルです。

cy.waitUntil(() = {
  return document.cookie.indexOf('hasCookie')
}), { timeout: 5000, interval: 200 }

3. スタブを使用して外部要因を排除する

テストする対象によっては使用できませんが、cy.server()cy.route() によるスタブを使用することで、ネットワークやリクエスト・レスポンス等の外部要因を排除したテストを行うことができます。

*Cypress 6.0.0 より、cy.server()cy.route() は非推奨となっています。今後は cy.intercept() を使用することが推奨されます。

cy.server().route({
  method: 'GET',
  url: '**/api/hoge?**',
  status: 200,
  response: {}
})
.as('hoge')
cy.wait('@hoge')
cy.request('@hoge')

最後に

仕様を理解して使うことが大事

%d人のブロガーが「いいね」をつけました。