UIを自動操作して実施する自動テスト、いわゆるE2Eテストは、「UI経由で出来るテストならだいたいなんでもできる」というのが素晴らしい点1なのだが、社内システムなどで古いWebアプリケーションライブラリを使っているとたまに自動テストツールを破壊することがある。
これはあくまで互換性の問題なので特定のライブラリをくさすつもりはない。ないのだが、特に古すぎるライブラリはテスタビリティを含めたソフトウェア開発の持続可能性を大きく損ねるので、ちゃんとマメにアップデートしておこう、メンテされてないライブラリへの依存は減らしておくといいことがあるぞ、という意図で読んで欲しい。
prototype.js
prototype.js のことを覚えているだろうか?Ajax隆盛期に一世を風靡したライブラリだ。2015年でメンテナンスがストップしてしまっているが、社内システムなどでは意外とまだ元気に動いていたりする。Claudeに聞いてみたところ、時期としては2006〜2010年ごろ、ちょうどjQueryが出るか出ないかぐらいの時期で、AgaxやDOM操作を簡単にするライブラリとして、特にRails開発者たちの間で良く使われいていたようだ。
さて、prototype.jsがなぜ自動テストツールを壊すかというと、ネイティブオブジェクトのPrototypeに直接メソッドを追加するからである。ここで追加されるメソッドの中には、 Array.reduce() や Array.toJSON() など、現代においてはどのブラウザでも標準で実装されているものも含まれている。prototype.jsはこれらを自前の実装で上書きしてしまうが、ネイティブの実装と必ずしも同じ振る舞いをするとは限らず、結果的に深刻なエラーを引き起こす。
Playwrightにもこれ関連のIssueがいくつか作られているが、流石に10年以上更新のないライブラリに起因するIssueということもあり、全てクローズされてしまっている。
- https://github.com/microsoft/playwright/issues/32507
- https://github.com/microsoft/playwright/issues/22460
AMP
SEO上有利ということで一時期めちゃくちゃ流行ったAMPだが、そんなAMPもやはり自動テストを壊すことがある。具体的には、 HTMLElement を上書きしており、判定が壊れる。
const el = document.querySelector('a')
> Object.getPrototypeOf(el)
HTMLAnchorElement {…}
> el instanceof HTMLAnchorElement
true // ここまでは想定通り
> el instanceof HTMLElement
false // HTMLAnchorElementはHTMLElementを継承している(プロトタイプチェーンにある)はずなのでこれはおかしい
> Object.getPrototypeOf(el.constructor.prototype)
HTMLElement {…} // 要素のprototypeはHTMLElementだ
> el.constructor.prototype instanceof HTMLElement
false // しかし instancof HTMLElement ではないので、ブラウザ標準のHTMLElementとは別人であることが分かる
これがクリティカルになるのは、例えば HTMLElement と SVGElement を分けて処理したいような場合だ。例えば自動テストツール側で以下のようなif文を書いてしまうと、本来 HTMLElement として扱いたかったDOM要素までelseに入ってしまうことになる。
if (el instanceof HTMLElement) {
// HTMLElement向けの処理
} else {
// 他のElement向けの処理
}
絵文字
これは割と有名かもしれない。ChromeDriverを通して絵文字を入力しようとすると ChromeDriver only supports characters in the BMP と怒られる。BMPはビットマップのことではなく、Unicodeの「Basic Multilingual Plane」(基本多言語面)2の略。
一応、ブラウザのJavaScriptに絵文字を打たせることはできる。
driver.execute_script("document.querySelector('#input').value = '😀';")
ここで気をつけないといけないのが、 input 要素の value を変えただけだと change イベントなど本来文字入力起点で発生するはずのイベントが発火しないため、これらも丁寧にJSで発火させてやる必要がある。
自動テストの方が弱いパターン
<dialog> 要素
<dialog> 要素はHTML5で登場した要素で、いわゆるモーダルダイアログを作成できる。
モーダルダイアログの特徴として、ダイアログ外の要素は一切触れなくなることが挙げられるが、これはブラウザ拡張機能から埋め込んでいるものも例外ではない。
E2Eテストツールの中にはレコードアンドリプレイ、つまり画面上での操作をそのまま記録してテスト手順を作るものがあるが、アサーションステップを入れたりツール独自の機能にアクセスしたりするためにDOM上にウィジェットを埋め込んでいることがある。dialog要素はこれらを完全に動かなくしてしまう。
ところで、ここまでで出てきたのはおおむね古いライブラリや過去に使われていた技術スタック、フレームワークなどとの互換性の問題だったが、 <dialog> はモダンなセマンティックタグの1つであり、むしろ自動テストツールが壊れるほうがおかしいと言える。問題は拡張機能のContent ScriptでDOMツリー内にウィジェットを差し込んでいることなので、それ以外の方法、例えばSide PanelやDevTools Panelを使えば解決する場合がある。
<input type="date">
いわゆる日付入力。dialog 同様、HTML標準のため、テストツール側がちゃんとサポートしていてほしい類のもの。ロケールによって入力フォーマットが異なるのだが、良く知られている通り日付の表示形式のバリエーションは国ごとに異なる上、言語と日付フォーマットが必ず対応するとは限らない。おまけに、OSによって、ブラウザ単位で変えられるものとOSの設定から変えられないものとが存在していたりする。
おまけに、これらのデフォルトの実装はブラウザごとに違うので、自動テストツールで入力する際にはかなり手の込んだ処理が必要になる。なお、 input[type="date"] の実体(value)はISOフォーマットのため、 el.value = '1999-07-07' のようにJSから入力することも出来るのだが、上述の絵文字の例と同様、 keydown keyup change あたりのイベントが軒並み発生しないので、丁寧に発火させてやる必要がある。以下は、そうせずにテストツール側でなるべくリアルなユーザー操作を再現させたい場合のチートシートである(2022年ごろの調査のため若干古いかもしれない)。
| ブラウザ | 挙動 |
|---|---|
| Chrome | 表示形式に従う。キャレットの移動は不要だが、デリミタでキャレットの移動扱いになる。 |
| FireFox | ISOフォーマットで入力する。 |
| Edge | 表示形式に従う。キャレットの移動は不要だが、デリミタでキャレットの移動扱いになる。 |
| Safari | 表示形式に従う。キャレットの移動が必須。デリミタでは動かないので、左右キーやTabで動かす。 |
| IE | ISOフォーマットで入力する(そもそもdateに非対応のため、通常のinputになる)。 |
| Mobile Safari | ISOフォーマットで入力する。 |
| Mobile Chrome | SendKeysでは入力できない。JavaScriptでvalueを直接更新する必要がある。 |
なお、ブラウザによっては年を6桁まで許容しているものがある。少なくともChromeにおいては10000年問題のことは考えなくて良いらしい。
かつて自動テストツールを壊していたもの
zoom
Web会議アプリではなく、CSSプロパティの方。任意の要素を拡大・縮小出来るので、ホームページ作りでお世話になった諸兄も多いことだろう。元々IEの独自プロパティだったものが各ブラウザにも移植された経緯で実装されていたようだ。しばらくの間どのブラウザでもdeprecatedだったのだが、なんと2024年になって奇跡の復活を遂げる。
MDNを見ると、かつて存在していた deprecated という文字がきれいに消えている。感慨深い。
https://developer.mozilla.org/ja/docs/Web/CSS/Reference/Properties/zoom
というわけで、本稿ではかつて deprecated だったころの zoom の話をする。
zoom の何がヤバかったかというと、 zoom したサイズは JavaScript側には一切反映されない ことが挙げられる。これが何を意味すかはだいたい想像がつくだろう。要素の座標を計算して実施する類の処理は基本的に全部狂ってしまっていた。
代表例として、要素単体のスクリーンショットを撮る処理が挙げられる。これはVRT(Visual Regression Test)などで良く使われるのだが、おおむね次のような順序で行われる。
- 画面全体のスクリーンショットを撮影する
- 要素の矩形座標を取得する(
element.width, element.height) - スクリーンショットの中から要素の矩形座標を切り取る
zoom を使っている場合、例えば2倍に拡大(縦・横がそれぞれ2倍)すると、JavaScriptから見えるサイズは実際にレンダリングされているサイズの左上1/4になってしまう。結果、画像の一部しか比較できない、ということになる。
また、自動テストにおいては「要素の特定の位置をクリックする」というような処理をしたいときがある。画像の一部にクリッカブルな箇所があるケースなどが代表的だ。こういった座標計算も全部狂う。
ところで、普段あまり意識しないことだが、テストツールはだいたい要素の中央をデフォルトでクリックするようになっている。例えば WebDriverのElement Clickの仕様 には次のように書いてある。
6.\ Let click point be the element's in-view center point.
この計算で使われる座標もやはり zoom によるレンダリング結果と一致しないため、要素を 0.5倍 すると、ちょうどクリック位置がレンダリング結果の右下部分と一致してしまう。端数の切り捨て条件によってはクリック可能な座標から外れてしまうため、クリックできない。
おしまい
ここには書ききれなかったが、疑似要素の中のテキストの検証がややこしかったりとか、自動テスト時のみ有効なブラウザオプションがテスト対象のWebアプリの秘孔を突いてたりとか、ログイン処理などで大量にリダイレクトを繰り返すとなぜかAppiumが固まる、みたいなのもあった。おれは一体何と戦っているんだ?3
自動テストについての話は、だいたいツールの使い方やテストそのものの質(網羅性、頻度、安定性、etc)の話などで終わってしまうのだが、たまにツールそのものに目を向けてみると、自動テストツールがテスト対象アプリ・テストベッド(ブラウザ)・自動操作ドライバなどが積み重なって出来ているのが分かって面白い。
明日は @____rina____ さんの記事です。お楽しみに!
-
素晴らしくもあり、同時に誤解を生みやすい部分でもある。「UIのテストはE2Eテストでないとできない」とか。 ↩
-
詳細はWikiを参照。https://ja.wikipedia.org/wiki/%E5%9F%BA%E6%9C%AC%E5%A4%9A%E8%A8%80%E8%AA%9E%E9%9D%A2 ↩
-
6年ぶり2回目 https://speakerdeck.com/tsuemura/kurosuburauzatesutofalsean-toan-toan?slide=26 ↩



