フューチャー技術ブログ

data-testidはいつ使うべきか?そもそも使うべきなのか?

Playwrightあるいはそのロケーターの元ネタとなっているTesting Libraryでは、DOMを指定する方法として data-testid 属性を扱ったクエリーを提供しています。どちらでも getByTestId(ID文字列) メソッドを使い、この属性値を使った要素の取得が行えます。しかし、ドキュメントを見ると、PlaywrightもTesting Libraryも、「他の手法が使えないときの最終手段」としています。

In the spirit of the guiding principles, it is recommended to use this only after the other queries don’t work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure or styling css class names. Learn more about data-testids from the blog post “Making your UI tests resilient to change”

方針の原則の精神に基づき、他のクエリではユースケースに合わなかった場合にのみ、これを使用することをお勧めします。 data-testid 属性の使用は(エンドユーザーが)ソフトウェアを使用する方法とかけ離れているため、可能であれば避けてください。とはいえ、DOM構造に基づいてクエリを実行したり、(テストのために)CSSクラス名を設定するよりもはるかに優れています。 data-testid の詳細については、ブログ投稿の 変更に対するUIテストの回復力の強化」を参照してください。

以前書いた技術ブログの記事では「人間に近い感覚」で要素を取得するテストが壊れにくいテストであるということを書きました。data-testid はHTMLを見て初めて知り得る情報ですので、E2Eテストではなるべく使うべきではありません。エンドユーザーはDOM構造を見てウェブサイトにアクセスするわけではありません。ユーザーが操作するのはブラウザで操作するウェブアプリケーションであり、レンダリングされたウェブページを見て操作します。エンドユーザーから見れば、DOMの構造は実装の詳細であって、開発者ツールを見ないとわからない情報です。実装の詳細はリファクタリング等で変更されることがありますが、より抽象度の高い操作はそれよりも「意図せぬ変更」にはなりにくいです。

単体テストでも同様に使うべきではありません。同じようにテストできるのであれば、テストコードは少ない方が良いし、ホワイトボックステストよりもブラックボックステストで公開メンバーのみに対するテストで済むならそちらの方が良いというのは多くの開発者が合意してくれる内容だと思います。公開メソッドで済むのにわざわざリフレクション機能を使うのはよくないですよね? DOM構造を使ったテストはなるべく行うべきではないホワイトボックステストです。

そうなると、Testing-Libraryの原則で説明されているように、他の方法がある場合はそちらを使うべき、というのがわかるでしょう。「data-testidなんて新しい属性を作らなくても、 idclass でいいのでは?」と思う方もいると思います。 idclassははテスト専用ではなくて別の役割も持っているため、テスト以外の動機によって変更されてしまうことがあります。 data-testid の立ち位置は、なるべく使うべきではないが idclass よりはまし、と覚えておきましょう。

唯一、気兼ねなく使っても問題がないと思われるケースは、単体テストかつ、これが外部に公開されたAPIである場合です。次のように、省略可能な data-testid 属性をコンポーネントに付与し、もし指定されたらコンポーネントのルートの要素にこの属性をフォワードして付与するようにします。

// data-testid属性をフォワードして設定するコンポーネント

function Component(props: {["data-testid"]?: string}) {
return <div data-testid={props["data-testid"]}>My Component</div>
}

こうすれば、単体テストにおいては、テストコードの見える場所で宣言と利用が行われます。「どこで定義されたかわからない謎の属性」感はなくなり、テストコードを読んだ人からはコンポーネントの中を知らずともその利用方法が想像できて、ブラックボックステストであるべき、という原則を壊さずに利用できていることがわかるでしょう。

// data-testidの問題ない利用例(Jest + @testing-library/react)

test('loads and displays greeting', async () => {
render(<Component data-testid="test-target" />)

expect(screen.getByTestId('test-target')).toHaveTextContent('My Component')
})

どちらにせよ、E2Eテストではなるべく使わない方が良いでしょう。眼に見える要素を使ってテストを書くべきです。

代わりに何を使えば良いか?

Testing LibraryもPlaywrightも、どちらもリファレンスでは同じような並びに並んでいることがわかります。アルファベット順ではないです。この順番で利用を検討していけば良いという推奨の順番と考えても良いと思います。(Testing-LibraryはByの前に、get, find, query, getAll, findAll, queryAllと6パターンの接頭辞のバリエーションがあります)

Playwright Testing-Library 役割
getByRole() ByRole() ロールで取得
getByLabel() ByLabelText() ラベルテキストで取得
getByPlaceholder() ByPlaceholderText() プレースホルダのテキストで取得
getByText() ByText() テキスト情報で取得
ByDisplayValue() input/textareaのvalue値で取得
getByAltText() ByAltText() 画像などの代替テキスト(alt属性)で取得
getByTitle() ByTitle() HTMLのtitle属性(ツールチップで表示される)で取得
getByTestId() ByTestId() data-testid属性で取得
locator() CSS/XPathで取得

檳榔さんから教えてもらったのですが、Cypressのベストプラクティスはdata-属性を付与することを推奨しています。

これは本エントリーとは矛盾はしません。というのも、アクセシビリティの要素での要素取得はCypress本体の機能ではなく、外部のライブラリのTesint-Libraryの機能だからです。Cypress本体の機能で実現できるのはgetByText()と、上記の表の末尾の2つです。このうち、テキストは変更されうるので非推奨としています。

アクセシビリティ要素に関して言えば、機能を起動する起点となるものはたいていロールを使います。テキストは結果の取得程度で、たいていは入力値と同じもの、あるいはそこから算出される期待するテキストの取得に使うと思うので問題はないでしょう。そんでもって末尾の2つの中ではdata-testidの方が優先度が高いのでCypressのドキュメントで書かれていることと矛盾はしないですよね?