E2Eテストにおける要素選択のベストプラクティス
こんにちは。Autifyの品質エバンジェリストの末村です。
もう5年前にもなりますが、なぜE2Eテストでidを使うべきではないのかという記事を書きました。この記事はありがたいことに長い間閲覧されており、弊社ブログの人気コンテンツの一つとなっています。
一方で、5年という歳月はソフトウェア開発のトレンドを変化させるには十分すぎるほどに長いです。テストツールの面では Playwright が新たな選択肢として加わった他、 testing-library によるアクセシビリティ属性を用いた要素探索が新たなベストプラクティスとして登場しています。
この記事では、E2Eテストにおける要素探索の考え方の変遷をおさらいしつつ、ベストプラクティスについて説明します。
要素選択の考え方
まずは、要素選択の考え方についておさらいしておきましょう。要素選択とは、読んで字の如く、Webページ内の特定の要素を探すことを指します。CSSセレクタや、XPathなどが使われることがあります。また、これらの要素選択に使われる文字列のことを総称して ロケーター と呼びます。
まずはCSSセレクタを使った例を見てみましょう。この場合は「送信」ボタンについている btn-primary
というクラスを用いています。
<button class="btn-primary">送信</button>
// btn-primaryというclassを持つ要素を指定する
driver.getElementsByclassName("btn-primary")
この例は、E2Eテストにおけるもっとも 原始的な 要素探索の例です。この手法には、 ロケーターの 一意性 と 内部構造の参照 という点で問題があります。
一意性
良いロケーターを構成する要素の一つとして、一意性 (Uniqueness) があります。これは、ロケーターにマッチする要素がページの中でただ一つである、ということを指します。
先ほどの例で利用した class
はボタンのスタイルに密接に関連し、ページ内で複数回登場する可能性がありますし、将来変更される可能性が高いです。 そのため、E2Eテストにおいては id
を使うことが長らくベストプラクティスとされてきました。これは、 id
は原則としてページ内で一意であることが定められているからです(参考: MDN https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/id)
<!-- submitというidを追加した -->
<button id="submit" class="btn-primary">送信</button>
// classではなくidでの指定に変える
driver.getElementByid("submit")
内部構造の参照
一意性の観点からは、 id
を使うのは良いアイディアに見えます。しかし、 class
にしろ id
にしろ、これらは基本的にHTMLの 内部構造 です。E2Eテストは、システムの 外的な振る舞い を確認するためのものですが、内部構造に依存する形でテストコードを書いてしまうと、外的な振る舞いが一切変わっていないにも関わらずテストコードを修正する羽目になってしまいます。
以下の例は、 id
を持つHTMLと、それを参照するテストコードです。
<label for="email">Email</label><input id="email" />
driver.getElementByid("submit")
<label for="email">Email
</label><input id="email" />
// Locate by ID
driver.getElementByid("submit")
このような書き方だと、何らかの 開発上の 都合 ─ 例えば、バックエンドの修正でフォームリクエストを変えたいようなとき、テストコードの方も書き換えないといけません。ユーザーに影響するような変更は一切加えていないとしても、です。
<label for="form-123">Email</label>
<input id="form-123" />
// Locate by ID
driver.getElementByid("form-123")
E2Eテストは 現実のユーザージャーニーを反映 したものであるべきです。しかし、このような書き方だと、E2Eテストは単に フロントエンドのコードをテスト するだけのものになってしまいます。
意味(セマンティクス)ベースのロケーター
では、 id
や class
の代わりに何を使えば良いのでしょうか?
先述の通り、E2Eテストはシステムの外的な振る舞いをテストします。別の言い方をすれば、E2Eテストは ユーザーがそのシステムをどのように使うか を、ブラウザ自動操作技術などを用いて動作するコードとして記述するものです。それゆえに、E2Eテストにおけるロケーターは、ユーザーがどのようにその要素を見つけるか に限りなく近い形であるのが理想的です。
テキスト
例えば、 要素内のテキスト が考えられます。ユーザーの多くは、テキストを頼りに要素を見つけます。 id
も class
も付いていない二つのボタンがあったとして、文言さえ違っていればユーザーは要素を特定できます。例えば、以下の例では、「次へ」「戻る」というテキストを用いて要素を特定できます。
<button>次へ</button>
<button>戻る</button>
役割
次に、要素の 役割 を用いる方法です。多くのHTML要素には 暗黙のロール(役割) が定義されており、これらをロケーターとして利用することが出来ます。
例えば、 <input type="button"/>
と <button/>
はタグ名こそ異なりますが、いずれも button
ロールを持っています。ユーザーからすれば、ボタンが内部的に input
タグで実装されていようと button
要素で実装されていようと関係ありません。
// これらはいずれも "button" ロールを持つ
<input type="button" value="次へ"/>
<button>次へ</button>
なお、暗黙のロールを持たない <div>
などのタグについても、 role
属性を付与することで、任意のロールを付与することが出来ます。
誤解しないようにしておきたいのは、ロールベースでの要素探索だからといって、常に role
属性が必要なわけではないということです。むしろ、要素探索のためにやみくもに role
を付与してしまうと、逆にアクセシビリティを損ねることに繋がりかねません。
なお、やみくもに role
を用いることの危険性については、 ymrlさんの記事が大変詳しいです。
https://qiita.com/ymrl/items/6c9c059208ea11e6d7bc
構造
次に、Webサイトの 構造 を用いる方法です。構造とは、「ある要素の中の別の要素」というようなものです。例えば、次のように2つの箇所に似た要素がある場合には、その親となる要素によって区別することが出来ます。
<nav>
<form>
<label for="email1"> Eメール </label>
<input id="email1" type="text" name="email"/>
<label for="password"> パスワード </label>
<input id="email2" type="password" name="password"/>
<input type="submit" value="Submit" />
</form>
</nav>
<main>
<form>
<label for="email1"> Eメール </label>
<input id="email1" type="text" name="email"/>
<label for="password"> パスワード </label>
<input id="email2" type="password" name="password"/>
<input type="submit" value="Submit" />
</form>
</main>
実装
Testing Library
これらのロールベースの要素探索を実装する最も手っ取り早い方法は、Testing Library を使うことです。Testing Library は、テストのベストプラクティスを実装するためのツールキットで、ロールベースの要素探索を実現するためのいくつかのAPIを提供しています。
Testing Library が提供するAPIには、以下のようなものがあります。
- getByRole
- getByLabelText
- getByPlaceholderText
- getByText
- getByDisplayValue
また、見つかった要素の中を探索するための within
というAPIもあります。これを使うと、上述のような構造ベースの探索ができるようになります。
Testing Library の良いところは、これらのAPIの利用優先順位についてガイドラインを提供しているところです。こうして考え方とソリューションをセットにして出してくれるのは嬉しいですね。
https://testing-library.com/docs/queries/about
Playwright
また、いくつかのE2Eテストフレームワークは、標準でtesting-libraryと同等のAPIをサポートしているものがあります。代表的なものがPlaywrightです。
https://playwright.dev/docs/locators
Playwrightもまた、 testing-library と同様の要素探索をサポートしていますが、少し書き方を簡単にするような工夫をしてくれています。詳細はTesting Libraryからの移行ガイドに書いてあります。
https://playwright.dev/docs/testing-library#replacing-within
Chrome DevTools
上述した getByRole
や getByLabelText
などを用いるには、対象の要素のロールやアクセシビリティ属性が分からないといけません。これらはDOMツリーには載っていないので、アクセシビリティツリーを見る必要があります。
例えば、以下のWebサイトは、ナビゲーションバーの中にフォームが入っており、その中にいくつかテキストボックスやボタンが入っているような構造になっています。このアクセシビリティ属性を確認するには、Chrome DevToolsでアクセシビリティツリーを確認するのが手っ取り早いです。
まとめ
この記事では、WebアプリケーションのUIテスト/E2Eテストにおける要素探索についての考え方の変遷と、現代のベストプラクティスを紹介しました。ユーザーにとってアクセシブルな属性、ユーザーにとってのセマンティクスを用いてテストコードを書くことで、テストコードがテスト対象の外的な振る舞いだけに依存するようになり、内部構造の変化に対して強くなり、安定したメンテナンス性の高いテストコードを書けるようになります。
最後に、 testing-library
の作者であり、Webフロントエンドのテストについての考え方を長らくリードしている一人である Kent C. Dodds 氏のポストを引用します。
テストがソフトウェアの実際の使われ方に似ていれば似ているほど、テストはあなたに自信を与えてくれるだろう。
この記事が、みなさんのテストを「実際の使い方」に即したものにする手助けになれば幸いです!