cypress で期待通りに自動テストが動かなかったときの対策集


前回の「cypress を使って自動テストを簡単に導入する方法」の続きです。

テストコードとブラウザーが非同期で動いていることに対応する機能とその限界

cypress など、ブラウザーを使って自動テストをする場合、
テストコードは、ブラウザーとタイミングを合わせながら非同期で動くのですが、
それが原因でテストコードが期待通りに動かないケースがよくあります。

たとえば、入力値を入れる GUI 部品が表示される前に入力しようとすると、
入力しないまま次のテストコードを実行してしまいます。
テストをすることは、入力した値に対して出力値が正しい値(期待する値)であることをチェックすることですから、
そもそも入力できなければ正しくテストを実行できません。

cypress では正しく入力できるように、対象の GUI 部品が表示されるまで自動的に待ってから
実行するように常に動いています。 たとえば cy.get メソッドが待ちますが、それ以外のメソッドでも待ちます。
フォームの項目に入力するときも入力が完了するまで待ちます。

通常、テストコードの実行はブラウザーがページをレンダリングする処理よりも速いため、
テストコードの内部では短い待ちが頻繁に発生しています。
しかし、この待ちを cy.get メソッドなどの内部で自動的に行っているおかげで、
テストコードに待ちを書く必要がなくなり、実施したいテストの仕様の記述に集中できるのです。

cy.get('#input-text')
    // ... <input id="input-text"/> がレンダリングされるまで待つ

しかし、表示されるまで自動的に待ったとしても、あらゆるケースで完全にタイミングが
合うとは限りません。

表示されるまで待つべきか、いつまでも表示されないのか

もし、cy.get メソッドに指定した ID が存在しない ID だったらどうなるでしょう。
HTML のレンダリングが完了しても対象の GUI部品は表示されずに待ち続け、
いずれタイムアウトのエラーになります。

ブラウザーから cypress に HTML のレンダリングが完了したことが通知されれば、
そのタイミングですぐに対象の GUI部品が見つからないというエラーにすることができるのですが、
そのような仕組みは無いようです。

よって、タイムアウトの原因は大きく分けて2つ考えられます。
- レンダリング中
- テストコードの実行に失敗した

ただし、これはタイムアウトの原因の一部にすぎません。

タイムアウトでエラーになる原因はいくつもある

cypress のタイムアウトの原因はいくつも考えられえます。

  • レンダリング中
  • 対象の GUI部品の指定が間違っている(対象の GUI部品が表示されるまで待っている)
  • 対象の GUI部品が非表示になっている
  • 入力しようとしている GUI部品が無効状態になっている
  • 対象の GUI部品が別の GUI部品の奥に隠れていて操作ができない
  • 対象の GUI部品がスクロールしないと見えない位置にある
  • マウスを合わせると(ホバリングすると)表示される要素が表示されない
  • 出力値を表示するまでに時間がかかっている
  • 誤った出力値が表示されている(期待する出力値が表示されるまで待っている)
  • 対象の GUI部品がアニメーションして動いているため、 クリックをブラウザーに要求してから実際にクリックされるまでの間に、 GUI 部品がクリックした位置から移動した
  • モーダル表示を閉じるアニメーションの途中でも、 閉じた後で操作する対象の GUI部品は表示されているため、 モーダル表示が閉じられる前に操作しようとして失敗した
  • タイムアウトしたところより前のテストコードの操作が失敗していたがスルーされていた

他にも原因があるかもしれませんが、今回これらが原因であるときの対処法を紹介します。
ただし、どれが原因であるかは、テストコードを実行したときのブラウザーの様子を目視で
確認して判断しなければなりません。その判断をした後での対処法になります。

レンダリング中のときや、出力値を表示するまでに時間がかかっているときの対処法

タイムアウトしたときにブラウザーのレンダリングや出力するまでの処理が完了していないようだったら、
タイムアウトの時間を延ばします。
特にページを移動するときにタイムアウトになる可能性が高いので、
ページの最初の GUI 部品を特定する cy.get メソッドに timeout オプションを指定します。
timeout オプションの値の単位はミリ秒です。

cy.get('#input-text', { timeout: 30000 }).should('have.value', 'ABC')

ただし、ページが表示されるまで待つコードのほうが可読性が高まります。
その場合、最後のほうでレンダリングする GUI 部品が表示されるまで待つコードを書きます。

cy.get('#last-button', { timeout: 30000 }).should('be.visible')

cypress の should メソッドは値のチェックだけではなく、
その状態になるまで待つという意味も含んでいると考えると読みやすくなります。
また、timeout オプションが書いてあると長時間待つ必要があるということも伝わります。

デフォルトのタイムアウトは 4秒ですが、
4秒では待っていないように見えるときでもタイムアウトになることが多いので 10秒にするとよいでしょう。
性能が悪い PC を使っているときは、もっと長くする必要があるかもしれません。

cypress.json

{
    "defaultCommandTimeout": 10000
}

参考: https://docs.cypress.io/guides/references/configuration.html#Timeouts

しかし、タイムアウトを長くしてもタイムアウトするまでにブラウザーに動きが全くなかったら、
タイムアウトの長さが原因ではないのでタイムアウトまでの時間を戻した方が素早く開発できます。

ちなみに、デバッグ対象の HTML のロードに時間がかかるときは、
ロードした HTML を cypress fiddle にコピーして実行すれば素早く
テストコードをデバッグできます。

cy.get に失敗するときや、対象の GUI部品の指定が間違っているときの対処法

cy.get に失敗するときは、
前回の「cypress を使って自動テストを簡単に導入する方法」で説明した Selector Playground
を使って typo を防ぎます。

ただし、

cy.get('#input-text')

または

cy.get('[data-test=input-text]')

のようなシンプルな cy.get ではないときは、前回説明したように、
id 属性または data- 属性(例:data-test 属性)を記述してください。

対象の GUI部品が非表示または無効状態になっているときの対処法

操作対象の GUI 部品が表示前または非表示または無効状態になっていると、
その GUI 部品は操作を受け付けません。
これは、手動でブラウザーを操作したときの仕様なのですが、
cypress でブラウザーを操作したときも同じです。

多くの場合、表示や有効状態にするための操作が正しく実行されなかったか、
テスト対象にバグがあることが原因です。

cypress は、対象の GUI 部品が非表示または無効状態のときは、
有効状態に変わるまで待ち続けます。
ビジー状態で各種 GUI 部品を無効状態にしているアプリケーションのときは、
無効状態ではなくなるまで自動的に待ちます。

テストコードに表示や有効状態であることを事前にチェックするコードを書く必要はありません。
次のようなコードではなく、

cy.get('#input-text').should('not.be.disabled')
cy.get('#input-text').clear().type('ABC')

次のようなコードを書きます。

cy.get('#input-text').clear().type('ABC')

同様に表示状態であることをテストコードに書く必要はありません。

cy.get('#input-text').should('be.visible')
cy.get('#input-text').clear().type('ABC')

表示状態になるまでの時間が長いときは、レンダリング中のときの対処法を参照してください。

別の GUI部品の奥に隠れているとき、スクロールしないと見えない位置にあるときの対処法

cypress でブラウザーを操作すると手動で操作したときで、
レイアウトやスクロール位置が異なることがあります。

ブラウザーの中のページのサイズを調整することで対象の GUI 部品が隠れないようにできるのであれば、
前回の「cypress を使って自動テストを簡単に導入する方法」で説明したように、
cypress.json の viewportWidth と viewportHeight を編集します。

常に手前に表示されるツールバーに隠れてしまい、スクロールすれば隠れないようにできるときは、

cy.get('[data-test=middle-button]').scrollIntoView()

の cy.get の対象となる GUI 部品の選択を工夫してスクロールさせます。

もしくは、force: true オプションで隠れた GUI 部品を強制クリックします。
ただし、奥に隠れている GUI 部品を操作することはできないという不具合は
検出できなくなるのであまりお勧めできません。

cy.get('[data-test=button]').click({ force: true })

マウスを合わせると表示される要素が表示されないときの対処法

cypress はマウスを動かしてホバリングすることをエミュレーションすることができません。

force: true オプションで表示されない GUI 部品をクリックします。

cy.get('[data-test=button]').click({ force: true })

誤った出力値が表示されている

おそらくテスト対象にバグがあります。
前回の「cypress を使って自動テストを簡単に導入する方法」で説明したデバッグ表示の方法を
使ってデバッグしてください。

ただし、後述するように、タイムアウトしたところより前のテストコードの操作が失敗していた
可能性もあるので注意してください。

対象の GUI部品がアニメーションして動いているときの対処法

cypress から起動したブラウザーの左半分に表示されるログの中にある click メソッドに
マウス カーソル を合わせると、クリックした位置が赤い丸で表示されるのですが、
クリックする対象となる GUI 部品がアニメーションして動いていると、
クリックした位置がずれて失敗することがあります。
Bootstrap のモーダル表示など表示を開始するときにアニメーションさせるとこの問題が発生します。

下記の waitForAnimation 関数を呼び出して、アニメーションが止まるまで待つことで、
クリックする位置がずれる問題に対処できます。

// waitForAnimation
// Example: waitForAnimation('[data-test="user-mail-address"]')
export function  waitForAnimation( getParameter ) {
    cy.get(getParameter).should('be.visible')
    cy.get(getParameter).then( (elements) => {
        const  element = elements[0];
        let    old = element.getBoundingClientRect();
        for (var i = 0; i < 10; i++) {
            cy.wait(100);
            const  new_ = element.getBoundingClientRect();
            if ( old.x === new_.x  &&  old.y === new_.y )
                break;
            }
            old = new_;
        }
        cy.wait(100);
    })
}

モーダル表示をアニメーションしながら閉じた直後の操作が失敗するときの対処法

これも Bootstrap などによってモーダルを閉じるアニメーションをするときによく発生する問題です。

対処するには、モーダル表示に含まれる GUI 部品が完全に非表示になるまで待ちます。

cy.get('[data-test=button]').should('be.not.visible')

HTML のイベントが発生しないときの対処法

<input onChange="onChange(event);"/>

の onChange のように on から始まるイベント属性や addEventListener メソッドで登録した
イベント ハンドラー が実行されないときは、
.trigger メソッドの呼び出しを書くと実行されます。
.trigger メソッドに指定するイベントの名前の先頭には on が付かないことに注意してください。

cmd.uploadFile( cy.get('[data-test="file-selector"]'), '申請書.docx')
cy.get('[data-test="file-selector"]').trigger('change')

参考: https://github.com/cypress-io/cypress/issues/1570

タイムアウトしたところより前のテストコードの操作が失敗していたときの対処法

テストコードが止まったところよりも前に原因があることは非常によくあります。

たとえば、入力値の設定が行われていなかったら、
出力値が誤った値になり、出力値をチェックするコードでタイムアウトになります。
よって、出力値が誤っていたからテスト対象のコードに不具合があると決めつける前に、
入力値が正しく入力されているかを目視でチェックしなければなりません。

他にも、対象のボタンが押せない原因は、対象のボタンが無効状態から有効状態に変えるための
別の GUI 部品の操作に失敗していたからかもしれません。
テスト対象が無効状態になっているのはバグだと決めつける前に
別の GUI 部品の操作が正しく実行されたことを目視でチェックしなければなりません。
この場合も、テストコードが止まったところよりも前に原因があります。

cypress はテストコードが止まったところよりも前の画面の様子を遡って確認することができます。
自動テストは操作が素早いのでじっくり見ることが難しいのですが、
cypress から起動したブラウザーの左半分にあるログにマウスカーソルを合わせると、
ログに表示されているテストコードを実行した瞬間にブラウザーに表示された様子が
右半分に表示されるためじっくり確認することができます。

cy.get にマウスカーソルを合わせると、cy.get に成功した GUI 部品が強調表示されます。

テストコードが正しく動いていることを確認したら、テスト対象のデバッグやバグ報告をしましょう。

その他、どうしてもタイミングが合わせられないときの対処法

待ちが終了するタイミングの条件がどうしても分からないときは、
一定時間待つことで動くようになるかもしれません。

cy.wait(500)  // Wait for 500 msec

ただし、一定時間待ってタイミングを合わせる方法は、
次回のテストで失敗する可能性があるため、あまりお勧めしません。

cy.wait を追加した今回は早くテストが通るようになりますが、
テストの実行時間が長くなってしまうことと、
後で時々動かなかくなったときに多くの時間が取られる可能性が高いです。
タイムアウトとは異なり、必ず指定した時間だけ待つため、
待つ時間を短くしたいという圧力もかかり失敗する可能性が高まります。
できるだけ cy.wait を使わないで済むパターンを学んでおくべきです。