自動E2Eテストにおけるテスタビリティ

## はじめに

:::note info
QAエンジニアの立場で、自動テストを導入する際の障壁をさげるために、プロダクトを開発するエンジニアに展開したい情報をこの記事にまとめる。
すでに、同様のことを述べている記事は世の中に複数あるが、自分の理解のためにも、自分の言葉で記述する。
また、「同じ意見を持っている」ことを示すためにも、自分の言葉で記述する。
:::

## 自動テストにおけるテスタビリティ

テスタビリティとは、テスト容易性・テスト実行可能性ともいい、テストが実行しやすいか、必要なテストが十分実施できるか、を表すものである。テスタビリティが高いということは、必要なテストが実行しやすく、不具合が見つかりやすいということになる。一方、テスタビリティが低いと、そもそもテスト実行が不可能だったり、実現できたとしても膨大なコストが発生したり、または、限られた方法でしかテストができないために不具合検出率が低くなる、ということが起こる。これらは、自動テストに限らずすべてのテストに当てはまる。自動テストにおいては、これらに加えて、「テストコードの実装のしやすさや、テストコードのメンテナンスのしやすさ、テストコードの壊れにくさ」も含まれる。

自動E2Eテストを書く上で重要なファクターの一つが、操作や検証の対象となる要素を指定する「ロケーティング」である。常に一意であり、変更される可能性が少ないロケータを用いることが重要で、そのようなロケータを指定できることが自動テストにおけるテスタビリティの要因になる。

## 自動テストでの心構え

自動テスト、特にE2Eでの自動テストは、どのようにテスト実行されるべきかという話。

フロントエンドの自動E2Eテストのライブラリである[Playwright](https://playwright.dev/)の公式ドキュメントのBest PracticeのページにTesting philosophy(テストの哲学)という節がある。
そこには[Test user-visible behavior](https://playwright.dev/docs/best-practices#test-user-visible-behavior)として以下のような記述がある。

> Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output.

要約すると、「エンドユーザーは、ブラウザがレンダリングしたアプリケーションを見て操作するため、自動テストでは、エンドユーザーの使い方でアプリケーションが動作するかを検証するべきである」と述べている。

一方で、フロントエンドの自動テストライブラリ[Testing Library](https://testing-library.com/)では、[テストがソフトウェアの使われ方に似てくるほど、より信頼性が高まる](https://twitter.com/kentcdodds/status/977018512689455106)という考えに基づき、「アプリケーションの使われ方に近い形でテストが書けるような、メソッドやユーティリティのみを公開するようにしている。」と明言している(以下、原文)。

> [*Guiding Principles - Testing Library*](https://testing-library.com/docs/guiding-principles/
)
> We try to only expose methods and utilities that encourage you to write tests that closely resemble how your web pages are used.

このように、自動E2Eテストでは、ユーザーの実際の使い方での動作検証を実行する必要がある。

## 自動E2Eテストにおけるテスタビリティの向上

### アクセシビリティの向上

自動テストのテスタビリティについて調べていると、「アクセシビリティを向上すると結果として、テスタビリティは向上する。」という考えがみられた。

- [「フロントエンドのテストは“不安定さ・壊れやすさ”との戦い」和田卓人×倉見洋輔×古川陽介が語る、アクセシビリティの重要性](https://logmi.jp/tech/articles/328087)
- ["想像をなるべく減らす"リーダブルなE2Eテスト読み書きに悩まないためのコンテキストとセマンティクスへの意識](https://logmi.jp/tech/articles/327322)
- [testing-library でユーザの気持ちになって書くフロントエンドのテスト](https://zenn.dev/tnyo43/articles/39e4caa321d0aa)

アクセシビリティが向上すると、機械、ブラウザや支援技術などのユーザエージェントにとってのコンテンツの読み取りやすさが向上する。ここで、機械にとってのコンテンツの読みやすさをマシンリーダビリティという。Webアプリケーションの自動テストも、機械がコンテンツを読み取り、操作することで、アプリケーションの動作を検査する。したがって、マシンリーダビリティの向上は、自動テストにおけるテスタビリティの向上に貢献する。すなわち、アクセシビリティの向上は、結果としてテスタビリティを向上させる。

もう少し具体的にアクセシビリティの向上についてみてみる。WebコンテンツやWebアプリケーションのアクセシビリティにおいて、重要な点は、意味的なHTML要素を使うことにある。これが意味することは、「できる限り、ふさわしい HTML 要素を、ふさわしい目的に使う」ということである。WebコンテンツやWebアプリケーションの開発には、[ネイティブの HTML の機能](https://developer.mozilla.org/ja/docs/Learn/Accessibility/HTML)を使用しつつ、必要に応じて[WAI-ARIA](https://www.w3.org/TR/2023/PR-wai-aria-1.2/)[^1]を使用して、アクセシビリティを向上することになる。

この「意味的なHTML要素を使うこと」は、「ユーザーの実際の使い方での動作検証を実行する」を助けてくれる。特に、WAI-ARIAで定義された[`role`属性](https://www.w3.org/TR/wai-aria-1.2/#role_definitions)が役に立つ[^2]。例えば、以下のようなコンテンツをテストをする場合を考える。

```HTML
<form action="/login" method="POST" enctype="application/json">
  <div>
    <label for="username-field">username</label>
    <input type="text" id="username-field" required/>
  </div>
  <div>
    <label for="password-field">password</label>
    <input type="password" id="password-field" required/>
  </div>
  <button id="login">Login</button>
</form>
```

「ログインページの`username-field`という`id`属性のついた要素にユーザ名を入力する」とした手順の場合、`id`属性は、ユーザにとっては、アプリケーションを使う上では、知る必要のない値のため、実際の使われ方からは離れてしまう。具体的なテストコードをPlaywrightで書いてみると以下のようになる。

```typescript
test.describe("id属性で要素を指定してログイン", () => {
  test("正当なユーザー名とパスワードでログインできる", async ({ page }) => {
    await page.getById("username").fill("myUsername");
    await page.getById("password").fill("myPassword");
    await page.getById("login" }).click();

    // expected to success to login
  });
```

一方で、「ログインページの`username`というラベルのついた入力欄にユーザ名を入力する」とした手順の方が、より実際のユーザ操作に近い手順となる。つまり、`role`属性が付加されている(ここでは`role=textbox`が付加されている)ことで、自動テストを実際の使われ方に近づけることができる。具体的なテストコードをPlaywrightで書いてみる。

```typescript
test.describe("role属性で要素を指定してログイン", () => {
  test("正当なユーザー名とパスワードでログインできる", async ({ page }) => {
    await page.getByRole("textarea",  { name: "username" }).fill("myUsername");
    await page.getByRole("textarea",  { name: "password" }).fill("myPassword");
    await page.getByRole("button", { name: "Login" }).click();

    // expected to success to login
  });
```

`role`属性を明示的に付加しなくても、意味的にふさわしいHTML要素を用いれば、ブラウザにより`role`属性が補完される。

[Testing Library](https://wtesting-library.com/)では、前述の「テストがソフトウェアの使われ方に似てくるほど、より信頼性が高まる」の考えに基づいて、ロケーティングに用いるクエリに[優先度](https://testing-library.com/docs/queries/about#priority)を設けている。この優先度に従えば、テストはソフトウェアの使われ方に近づく。また、プロダクションコードでは、この優先度で上位にあるクエリで要素を指定できるように実装することを心がけるとよい。
この優先度では、`role`属性を指定して要素を取得する[API](https://testing-library.com/docs/queries/byrole/)が最優先と定義されている。

### data 属性

ロケータに、`id`や`class`、`name`といった属性これらを利用することは、アンチパターンと考えられる[^3]。主な理由は、以下の通り。

- `id`等のプロダクションコードの内部的な値を外部的なテストコードが参照している場合に、プロダクションコードの修正の容易性が損なわれる。
- プロダクションコードを修正した場合の、テストコードのメンテナンスが都度発生する。変更に弱いテストコード。メンテナンスコストが高いテストコード。(一方で、テストコード自体を、プロダクションコードの変更に影響されづらいように工夫する必要は、別問題としてある。)

そこで、[カスタムデータ属性](https://developer.mozilla.org/ja/docs/Learn/HTML/Howto/Use_data_attributes)([`data-*`](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/data-*))を使用することで、上記の問題を回避しつつ、以下のメリットが得られる。

- テストで用いられるカスタム属性であることが明確なため、プロダクションコードとテストコードとの関係が明確になる。
- 変更される可能性が低く、一意性のあるロケータとなる。

ちなみに[Testing Library](https://wtesting-library.com/)には、`data-*`属性を指定して要素を取得する[API](https://testing-library.com/docs/queries/bytestid/)が提供されている。また、[Cypress](https://www.cypress.io/)の[Best Practice](https://docs.cypress.io/guides/references/best-practices#Selecting-Elements)としても、`data-*`属性をセレクタとして使用することが推奨されている。
一方で、前述した「テストがソフトウェアの使われ方に似てくるほど、より信頼性が高まる」という考えから、[Why You Should Avoid Testing React Components With Test IDs](https://betterprogramming.pub/why-you-should-avoid-testing-react-components-with-test-ids-ee50d20d37d2)では`data-*`属性をロケータに使用しない方がよいと述べている。ユーザーはその要素がどんな属性(ここでは`data-*`属性)を持っているかを知らなくても、操作できるので、この考え方に従わないためである。また、`data-*`属性はプロダクションコードに影響せずに、`id`の代替として有用ではあるが、結局管理コストがかかる点で問題がある。それでも、前述のメリットがあるので、ないよりはあった方がいいとは思う。

## まとめ

- Web フロントエンドの自動テストでは、ユーザーの実際の使い方での動作検証を実行するべきである。
- Web フロントエンドの自動テストにおけるテスタビリティの向上にはプロダクションコードのアクセシビリティが重要になる。
- `data-*`属性は、その使用目的を明確にでき、変更される可能性が低く、一意性のあるロケータとなるが、上記の方針に従わなくなる。しかし、`id`属性等を用いるよりはよいため、あった方がいい。

[^1]: MDNの[ARIA](https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA)や[WAI-ARIAの基本](https://developer.mozilla.org/ja/docs/Learn/Accessibility/WAI-ARIA_basics)は参考になる
[^2]: その要素がどんな役割をもつか、もしくは何をするかを明確にする
[^3]: [idやclassを使ってテストを書くのは、もはやアンチパターンである](https://qiita.com/akameco/items/519f7e4d5442b2a9d2da)や[なぜE2Eテストでidを使うべきではないのか](https://autify.com/ja/blog/why-id-should-not-be-used)や[Making your UI tests resilient to change](https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change)でも述べられている