QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Seleniumで壊れにくいロケータを設計する:絶対XPathをやめて変更に強い要素指定に

Selenium・自動化

DevToolsからコピーした絶対XPathがすぐ壊れる理由

Chrome DevToolsの要素検証パネルには「Copy XPath」という機能があり、これを使うと次のような文字列がクリップボードに入ります。

# アンチパターン: DevToolsからコピーした絶対XPathをそのまま使う
submit_button = driver.find_element(:xpath, "/html/body/div[3]/div[1]/section/div[2]/form/button")

一見すると確実に対象を指しているように見えますが、これは「ルートから何番目の子要素か」という位置情報の連続でしかありません。ページの上部にお知らせバナーが1つ増える、A/Bテストで見出し用のdivが追加される、フォームの前に注意書きのブロックが挿入される、といった、機能とは無関係な変更が起きただけで、div[3]div[4]に変わってしまいます。

厄介なのは、この手の変更がリリースノートに載らないような些細な修正で頻繁に起きることです。絶対XPathで組んだテストは、対象の機能自体は何も変わっていないのに、無関係な箇所の修正のたびに壊れて原因調査に時間を取られます。

自動生成されたclass名に頼らない

同様に危険なのが、CSS ModulesやCSS-in-JS、Tailwindのパージ処理などが生成するハッシュ化されたclass名です。

# アンチパターン: ビルドのたびに変わるハッシュ化class名に依存する
submit_button = driver.find_element(:css, ".sc-bZQynM.css-1x2y3z")

sc-bZQynMcss-1x2y3zのような文字列は、スタイルの衝突を避けるためにビルドツールが自動採番したものです。見た目やロジックを一切変えなくても、依存パッケージの更新やビルド設定の変更だけでこの値は変わり得ます。DevToolsで見えているclass名をそのまま貼り付けると、開発側が意図していない箇所でテストが壊れる典型パターンになります。

data-*属性とテキスト・ラベルとの関係を頼りにする

壊れにくいロケータの基本方針は、「スタイリングのための属性」ではなく「識別のための属性」を使うことです。理想的なのは、テスト用にdata-testidのような属性を開発側に用意してもらうことです。

submit_button = driver.find_element(:css, "[data-testid='contact-submit']")

data-testidはスタイル変更やリファクタリングの影響を受けないため、ページ構造やクラス名がどれだけ変わっても指し先がぶれません。ただし、既存のフォームに後付けで属性を追加してもらえない場合も多くあります。その場合は、画面上に表示されているラベルやテキストとの「関係」を頼りにします。

相対XPathでテキストや構造の“関係”を指定する

div[3]のような絶対位置ではなく、「送信という文字を含むボタン」のように、意味のある内容との関係で要素を指定するのが相対XPathの使いどころです。

submit_button = driver.find_element(:xpath, "//button[contains(., '送信')]")

contains(., '送信')としているのは、ボタンの中に<span>などの子要素が入っていてテキストが直下ではなく孫要素にある場合でも拾えるようにするためです。text()ではなく.を使うことで、要素配下の全テキストを対象にできます。

ラベルと入力欄がプログラム的にはfor属性で結びついていない古いフォームでも、見た目上の並び順を使って辿れます。

email_input = driver.find_element(
  :xpath,
  "//label[contains(., 'メールアドレス')]/following-sibling::input"
)

これは「メールアドレスというラベルの直後にある入力欄」という、人間がフォームを見たときの理解と同じ関係を使っているため、DOM構造が多少組み替わっても、ラベルと入力欄の並びさえ保たれていれば動き続けます。

複数条件で絞り込み、誤爆を防ぐ

テキストや属性1つだけで絞り込むと、同じページに似た要素が複数あるときに意図しない要素を拾ってしまうことがあります。フォームが複数並ぶページで単に「送信」というテキストのボタンを探すと、別のフォームの送信ボタンまでヒットしてしまう、といった具合です。

# 「送信」というテキストのボタンが複数存在し、誤爆する可能性がある
driver.find_element(:xpath, "//button[contains(., '送信')]")

# 属性とテキストを組み合わせて対象を絞り込む
driver.find_element(
  :xpath,
  "//form[@data-form-name='contact']//button[@type='submit'][contains(., '送信')]"
)

@type='submit'という属性条件と、contains(., '送信')というテキスト条件、さらに//form[@data-form-name='contact']という祖先要素の限定を組み合わせることで、「お問い合わせフォームの中にある、送信タイプのボタンで、かつ送信という文字を含むもの」というように対象を一意に絞り込めます。条件を1つ増やすごとに指し先が明確になり、ページ内に似た要素が増えても誤って別の要素を操作するリスクが下がります。

ロケータをメソッド・定数に集約して保守性を上げる

ロケータの文字列をテストコードのあちこちに直接埋め込んでいると、セレクタが1つ変わっただけで複数ファイルを検索して修正する羽目になります。ロケータは1箇所にまとめておき、テストコードからは名前を通して参照するようにします。

module ContactFormLocators
  SUBMIT_BUTTON = { xpath: "//form[@data-form-name='contact']//button[@type='submit'][contains(., '送信')]" }
  EMAIL_INPUT   = { xpath: "//label[contains(., 'メールアドレス')]/following-sibling::input" }
end

def find(driver, locator)
  strategy, value = locator.first
  driver.find_element(strategy, value)
end

find(driver, ContactFormLocators::SUBMIT_BUTTON).click
email = find(driver, ContactFormLocators::EMAIL_INPUT)

こうしておくと、対象サイトの改修でボタンのテキストが「送信」から「送信する」に変わったときも、修正するのはContactFormLocatorsモジュールの1行だけで済みます。呼び出し側のコードは何も変えなくてよいため、複数のテストシナリオで同じ要素を参照していても、修正漏れが起きません。ロケータを定数化することは、単に見た目を整理するだけでなく、「壊れたときの修正コストを1箇所に閉じ込める」という保守性の設計そのものです。

まとめ

  • DevToolsの「Copy XPath」で得られる絶対XPathは、ルートからの位置情報でしかなく、機能と無関係な変更でも簡単に壊れる
  • CSS-in-JSやTailwindのパージが生成するハッシュ化class名は、ビルドのたびに変わり得るため識別子として使わない
  • data-testidのような識別用属性があれば最優先で使い、なければテキストやラベルとの関係で要素を指定する
  • 相対XPathのcontains(., "テキスト")following-siblingを使えば、絶対位置ではなく意味のある関係で要素を辿れる
  • 属性・テキスト・祖先要素などの条件を組み合わせることで、似た要素が複数あるページでも誤った要素を操作するリスクを下げられる
  • ロケータの文字列はテストコードに直書きせず、モジュールや定数に集約する。壊れたときの修正箇所を1つに閉じ込められる

コメント

タイトルとURLをコピーしました