壊れにくいXPathを生成するアルゴリズム Robula+

遅めの Selenium Advent Calendar の13日目のエントリーです。

Selenium を使ったテストを運用していると、ちょっと前まで使えていた HTML 要素のセレクタが使えなくなり、テストが失敗するようになることがあります。
ID が付与されていない要素を絶対パスの XPath で取得しているケースでは、テスト対象がアップデートするたびにセレクタもアップデートしなければいけなくなったり。
Selenium のテストの壊れやすさはセレクタの書き方次第といっても過言ではないとさえ思っています。

このエントリーでは、2016 年にジェノバ大学の方が「[ROBULA+: An Algorithm for Generating Robust XPath Locators for Web Testing.](http://dx.doi.org/10.1002/smr.1771)」という論文で発表した Robula+ というセレクタ生成アルゴリズムを紹介します。
Robula+ は要素の属性や順番を組み合わせて極力シンプルで壊れにくい XPath を生成します。
このアルゴリズムで生成された XPath は、ベタに書いた絶対指定のXPathより 90%、Selenium IDE が生成するセレクタより 63% 壊れにくかったそうです。
すこし前に発表されたものですが、HTMLのどの属性がどのくらい安定しているかも考慮されたアルゴリズムであるため、Robula+ を直接使わなくてもセレクタを書く上で参考になるものだと思います。

## XPath について

XPathで壊れにくいセレクタ！？という感じもあるので、Robula+ のアルゴリズムを紹介する前に XPath について少し触れます。
Selenium のテストで XPath は最後の手段として扱われがちな気がしますが、XPath 自体は非常に表現力の高い構文です。
XPath は XML や HTML のあらゆる属性や親子関係を組み合わせて要素を選択することができます。
Selenium の By.~~~ で取得できる要素はすべて By.xpath で置き換えられるはず。適切に使えば十分に壊れにくくできます。

ちなみに Selenium で使用できる XPath は XPath 1.0 。
Selenium の元となる [WebDriver](https://www.w3.org/TR/webdriver/#bib-XPATH) という仕様に基づいているようです 。
XPath 2.0 以降の仕様だと正規表現マッチが使えるようになってたりといろいろ便利なのですが...

## Robula+ のアルゴリズム

前置きが長くなりましたが、本題の Robula+ の説明に入ります。
Robula+ は `//*` という何にでもマッチするXPathから始まって、マッチする要素が 1 つになるまでXPathを厳密にしていくアルゴリズムです。
厳密にしていくときの優先順位は「タグ名 > ID > 要素の中身のテキスト > `name`, `class`, `title`, `alt`, `value` 属性｣です。
ちなみに `href`, `src` などの属性は変わりやすいことが分かっているため使わないそうです。
生成の手順は以下のような感じ。

----

1. `//*` というセレクタからスタート
2. もともとのセレクタから厳密にしたセレクタをいろいろなバリエーション作成
    1. 先頭の `*` をタグ名に変更 ( `//*` → `//td` )
    2. 先頭の要素に ID を追加 ( `//* `-> `//*[@id="hoge"]`)
    3. 先頭の要素にテキストを追加 ( //* -> `//*[contains(@text, 'nakami')]`)
    4. 先頭の要素に属性を 1 つ追加 (`//*` -> `//*[@class="fuga"]` と `//*[@name="piyo"]`)
    5. 先頭の要素にありったけの属性を追加 (`//*` -> `//*[@class="fuga", @name="piyo"]`)
    6. 先頭の要素にポジションを追加 (`//*` -> `//*[2]`)
    7. 階層を追加 (`//*` -> `//*/*`)
3. 各バリエーションの XPath が探したい要素にのみマッチするかチェックし、マッチしなかったら各バリエーションをもとに2,3の手順を繰り返し

----
例えば Robula+ で XPath を生成すると、この記事のタグの部分（青い点線で囲まれた部分）は ```//*[@class='it-Tags']/*[1]``` という XPath になります。 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/151403/f3ea80cd-f1c4-2f39-90ac-3fc7372fbe22.png)

Chrome DevTool の機能で生成すると ```//*[@id="main"]/article/div[1]/div/div[2]/div[1]/div[1]/div[2]/a``` となるのに比較すると、結構いい感じなのではないかと思います。

Robula+、このようにシンプルなアルゴリズムですが効果も検証済みです。
複数の OSS のウェブツールでメジャーバージョンが上がった時にセレクタが使えなくなる割合を検証したところ、[FirePath](https://addons.mozilla.org/ja/firefox/addon/firepath/) で生成した XPath より 90%、 Selenium IDE の生成するセレクタより 60% は壊れる割合が低かったそうです。

このアルゴリズムの Java での実装は論文著者の所属する研究室のウェブサイトに掲載されています。Firefox プラグインもあるのですが残念ながら最新のFirefoxでは動作しなくなっているようです。

http://sepl.dibris.unige.it/ROBULA.php (一番下のところです)

実装してみたい方はぜひコードを参考にされてください。この記事の説明が正しいか少し自信がありません。


## まとめ

このエントリーでは、Robula+ というアルゴリズムについて紹介しました。
Robula+、アルゴリズム自体もですが、属性を追加する順番に納得させられる部分が多かったです。
確かに ID 以外の属性より要素の中の文字列のほうが壊れにくかったりするよなーとか、たしかに src 属性は壊れやすいよなーとか。

これも参考にしつつ、XPathもうまく使いこなして壊れにくいテストを書けるようにしたいものです。


## おまけ

Chrome DevTool や Firefox の開発ツールにも XPath 生成の仕組みがあります。
Chrome のほうは直近の ID の振られている要素からの相対パスになるようで、Robula+ ほどではないにしろ、けっこう使えそうな感じです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/151403/63ddba89-dde5-3d83-a24b-713c0fe77568.png)