見出し画像

Seleniumのブラウザ要素スクリーンショットを再考する

こんにちは。自動化エンジニアの森川です。
本日はSeleniumを使ったブラウザ要素のキャプチャ(スクリーンショット)取得について考えたいと思います。

E2Eテストでも部分的なアサーションだけではなく、画面のレイアウトや可視的な要素を検証する場面がでてきます。いわゆるVisual Testingともいえるスクリーンショットの比較をすることになります。

ネット上の記事でも散々語られてきたSeleniumのスクリーンショット取得ではありますが、今一度考えてみたいと思います。

<本記事のインデックス>
・どうしてブラウザ要素のスクリーンショットが要るのか? 
・画像比較によるアサーションのメリット・デメリット
・ブラウザ要素のスクリーンショットを用いた検証について
・基本的な実装
・拡張的な実装
・まとめ

どうしてブラウザ要素のスクリーンショットが要るのか

一般的にGUI自動テストのアサーションはシンプルにする場合が多いです。
自動テストは初期・保守ともにコストが高いので、機能テストとスコープと役割を明確に分けて、必要最小限にしましょうというセオリーがあるからです。

レイアウトや目視のチェックは自動テストツールには向かないので、大抵の場合、これらをURLや表示文言のアサーションで置き換えることになります。

ところが、ステージング環境や本番環境では予期せぬ不具合が発生するものです。CSSの読み込み失敗や、イメージ要素がロードされないといった不具合もその一つです。

『水際のE2Eテストで見つけてくれたら…』

テスト自動化の前提をひっくり返すような要望がふつふつと現場チームから湧き出します。

『人間が目視テストしていたらこんなことには…』

自動化チームがザワザワとなる瞬間ですね。テスト自動化エンジニアとしては、なんとか挽回したいところです。

対策として表示文言やHTMLソースのアサーションを増やす手もありますが、保守性を考えるとあまりおすすめできません。
そこで画像比較による検証という方法が浮かびあがります。

画像比較によるアサーションのメリット・デメリット

一般的な画像比較によるアサーションのメリット・デメリットは以下のとおりです

メリット
1. CSS崩れを検知できる
2. 文言だけでなくフォント・文字サイズ・色といった可視的要素を検証できる
3. 画像がロードされていることを検証できる

デメリット
1. 想定差分(広告などの毎回変化する部分)と検出差分の切り分けが必要
2. 比較処理に時間がかかる
3. 比較元(お手本)画像の準備が必要
4. プロダクトの変更の影響を受けやすい

デメリットの1つ目については、全画面比較する場合は広告などのアクセス毎に変わる部分を想定差分として無視する必要があり、これらを除外するのは手間がかかりますが、比較する要素を限定する、つまりHTML要素で切り取ることで回避できそうです。
2つ目は、切り取るHTML要素の大きさにもよりますが、ブラウザ要素のみの比較ならば、全体を比較するよりは処理時間を押さえられそうな気がします。
3つ目については、自動テスト作成中の試行時に比較元(お手本)画像を自動取得することである程度負荷が軽減されることが予想されます。
4つ目は、画像比較のアサーションだけでなく自動テスト全体の問題ですし、3つ目同様、比較元(お手本)画像の作成・コミットフローを効率化することである程度解消すると思われます。

以上より「ブラウザ要素のスクリーンショット」を取得して検証に用いるという案に至るわけです。

ブラウザ要素のスクリーンショットを用いた検証

一般的なECサイトを例にします。

画像1

上図のような注文確認画面で、検証したいのは「金額表示」の部分と、「配送情報」の部分とします。
ブラウザ要素として切りだしてみるとこのような感じです。

画像2
画像3

これらをプログラムで切りだします。

基本的な実装

それでは実装してみましょう。

実行環境
・Selenium: Selenium4.0.0 alpha-6
・Java: AdoptOpen jdk-11.0.7.10-hotspot
・Browser: Google Chrome 86

Selenium4でHTML要素をキャプチャ

要素のスクリーンショット取得機能が強化されているという話※1ですのでSelenium4を使ってみました。
ご覧の通り、Seleniumによる実装は非常に簡単です。

WebElement element = driver.findElementByCssSelector(“div.your_locator”);
File screenshot = element.getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(screenshot, new File(“path-to-images/elementshot.png”));

採れたスクリーンショットはこちら

画像4

「確認する」、「カートに戻る」ボタンが表示されていませんね。何かがおかしいようです。(Selenium4で刷新された要素キャプチャでは、一時的な処理を施してキャプチャを撮っているようです。その際にこれらのボタンがついてきていない、そんなイメージです。)

aShotでHTML要素をキャプチャ

Seleniuimのスクリーンショットユーティリティとしては著名なaShotというOSSライブラリを使ってみます。

実装はこちら

WebElement element = driver.findElementByCssSelector(“div.your_locator”);
Float dpr = getDpr(); // 実行環境のDPR値を取得(ユーティリティメソッド)
BufferedImage image = new AShot()
 	.shootingStrategy(
 		ShootingStrategies.viewportRetina(100, 0, 0, dpr)
 	)
 	.takeScreenshot(driver, element).getImage();
// ユーティリティメソッドでBefferedImageをファイル化
writeImage(image, new File(“path-to-images/elementshot.png”));

採れたスクリーンショットはこちら

画像5

いい感じで採れていますね。さすがaShotです。

自前のHelperでHTML要素をキャプチャ

自前でHelperを書いてみました。(ユーティリティメソッドについては掲載を割愛しています。)

int w, h;
Float dpr = getDpr();
BufferedImage fullImage = ImageIO.read(driver.getScreenshotAs(OutputType.FILE));
Map<String, Long> elementPosition = getPositionByJS(element);
Long x = elementPosition.get("left");
Long y = elementPosition.get("top");
// 要素サイズ
w = element.getSize().getWidth();
h = element.getSize().getHeight();
// 切り取り
return fullImage.getSubimage(
       (int) (x * dpr),
       (int) (y * dpr),
       (int) (w * dpr),
       (int) (h * dpr)
);

採れたスクリーンショットはこちら

画像10

aShotで実行した場合とほぼ同じです。違いは画像サイズで、こちらは少し大きくなりました。これは筆者の実行環境のDPR値1.25が反映されていたためで、対してaShotではDPR値からピクセル=1.0dpiに自動補正してくれるためと思われます。
※ わざわざ自前でヘルパーを実装する理由は、後述の拡張的な実装のところで必要となるからです。

拡張的な実装

さて、ここからもう少しレベルを上げてみましょう。
試したみたのは以下の2パターンです。

・表示ウインドウよりも大きい範囲のHTML要素のスクリーンショット取得
・フレーム内のスクロール要素のスクリーンショット取得

表示ウインドウよりも大きい範囲のHTML要素のスクリーンショット取得

先述の注文確認画面の赤枠部分(下端は見切れていてウインドウよりも大きな要素です)のHTML要素のスクリーンショットを取得することにします。

画像7

Selenium4では残念な結果になりました(結果は割愛します)
aShot、自前のHelperともに以下のような結果となりました。

画像6

ウインドウ高さよりも大きなサイズの要素が、しっかりと採れていると思います。
※ 自前のHelperは上述の実装から少し手を入れています。

フレーム内のスクロール要素のスクリーンショット取得

インナーフレーム内のスクロール要素のスクリーンショットを取得するには、スクロールと要素の取得を繰り返して行って画像を結合するという処理が必要になります。
とある自治体のWebページのTwitterウィジェットで試してみたいと思います。
Twitterのタイムラインを検証するシチュエーションはあまり無いと思いますので例としては適切ではありませんが、例えば「インナースクロールになっている検索結果テーブルを検証したい」というような場合を想像していただけると良いかと思います。

画像8

埼玉県朝霞市公式WEBページより(https://www.city.asaka.lg.jp/)

Selenium4、aShotともにインナーフレーム内のスクロール要素取得はサポートと思われるため、自前で作成することになります。
先程の自前のHelperを子メソッドとして流用して、実装はおおおそ以下の通りとなります。(ユーティリティメソッドについては掲載を割愛しています。)

WebElement element = driver.findElementByCssSelector("#top_tw>div") // cssでoverflow:scroll(またはauto)となっている要素
Float dpr = getDpr();
// スクロール分高さ、可視範囲高さを取得
int scrollHeight = getIntValue(jsExecutor, "scrollHeight", element);
int clientHeight = getIntValue(jsExecutor, "clientHeight", element);
int clientWidth = getIntValue(jsExecutor, "clientWidth", element);
BufferedImage finalImage = new BufferedImage(
       (int) (clientWidth * dpr),
       (int) (scrollHeight * dpr),
       BufferedImage.TYPE_3BYTE_BGR
);
Graphics2D graphics = finalImage.createGraphics();
int scrollTimes = (scrollHeight - 1) / clientHeight + 1;
int lastTop = getPositionByJS(element).get("top").intValue();
// スクロール展開
for (int i = 0; i < scrollTimes; i++) {
   scrollElementVertically(jsExecutor, element, i * clientHeight);
   //補正値生成
   int currentTop = getPositionByJS(element).get("top").intValue();
   int adjustHeight = getAdjustHeight(lastTop, element);
   BufferedImage part = _elementShot(element, adjustHeight); // 上述の自前Helperを子処理として呼び出し
   graphics.drawImage(part, 0, (int) (getCurrentScrollY(jsExecutor, element) * dpr), null);
   lastTop = currentTop;
}
graphics.dispose();
writeImage(finalImage, new File(path));

採れたスクリーンショットはこちら

画像9

結合部分にラインノイズが入ってしまいましたが、おおむね採れていると思います。

おおまかな処理の流れとしては以下となります。

・フレーム内部のスクロールサイズ、可視化されたサイズをそれぞれJavaScriptで取得します
・そのスクロールサイズでイメージ枠(全体)をバッファします
・スクロール回数と一回の移動量(ピクセル)を決定します
・上から下までスクロールさせながら、可視化範囲のスクリーンショットを取得します
・取得したイメージを上述のイメージ枠(全体)に流し込んで結合します(これをスクロール終わりまで繰り返します)

上述の「基本的な実装」の場合と比べて、かなり複雑な処理になりましたね。
今回は縦スクロールのみでしたが横スクロールが必要な場合は、さらに複雑になります。

まとめ

ブラウザ要素のスクリーンショットの必要性から、拡張的な実装方法までご説明させていただきました。
SeleniumやaShotといったOSSのライブラリを組み合わせることである程度実装は容易になりますが、WEBサイトの構成によっては手間のかかるカスタマイズが必要になりそうなことがわかります。
実際の運用ではさらに以下のような課題が発生する可能性があります。

・スクロールサイズ、可視化範囲のサイズの取得(JavaScript)はブラウザによって異なるためにそれを考慮しなければならない
・スクロール処理の完了タイミングについても同様
・環境によってDPR値が異なるので補正が必要
・固定ヘッダ・フッタがあるページでは写り込まないように設定が必要、スクロール毎にそれらの補正値を考慮する必要がある

現場ではこれらを一つ一つ解消するために調査・対応するのはなかなか骨のおれる作業です。
作業見積りに調査工数は含まれない場合が多いので、エンジニアに求められるスキルセットは相応に高くなってしまいます。
これらは業務経験と蓄積したナレッジでカバーすることになります。

最後に少し宣伝です。

弊社のGUI自動化ツールRacine(過去記事参照)では、上述のHTML要素キャプチャ機能がすでに実装されていますので、テストコード作成と同時にご利用いただけます。お客様のテストサイトにあわせて、自動化アーキテクトが過去のナレッジを生かした柔軟な対応をすることで、最適なE2E自動テストを提供いたします。

Seleniumベースでの画像比較、要素比較といったVisual Testing的なアプローチをご検討の場合は、是非とも弊社の窓口にご相談ください。
テスト自動化支援 サービスのご紹介| 株式会社SHIFT

__________________________________

執筆者プロフィール:森川 知雄
中堅SIerでテスト管理と業務ツール、テスト自動化ツール開発を12年経験。
SHIFTでは、GUIテストの自動化ツールRacine(ラシーヌ)の開発を担当。
GUIテストに限らず、なんでも自動化することを好むが、
ルンバが掃除しているところを眺めるのは好まないタイプ。
さまざま案件で自動化、効率化による顧客への価値創出を日々模索している。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!