Seleniumで自動テストが失敗した時にハードコピーを取得する


webアプリのテストをselenium+Jenkinsで自動化していますが、seleniumのエラーレポートだけだと何が原因でエラーになったのか判断するのが難しいです。
そこで、テストが失敗した時に自動で画面のハードコピーを取得するようにしました。

まず、TestWatchクラスを継承したクラスを作成します。
public class SeleniumTestWatcher extends TestWatcher

静的初期化子を使って、テスト起動時に画像格納用のフォルダを作成するようにします。
Jenkinsから起動した場合とローカルでテストした場合で格納先を分けており、
Jenkinsから起動した場合はSystem.getProperty("BUILD_NUMBER"); でjenkinsのビルド番号を取得しています。

private WebDriver driver;

private static String imgpath;

static {

    if(imgpath == null) {
        File jenkins = new File("C:\\Program Files (x86)\\Jenkins");
        // for server
        if(jenkins.exists()) {
            String buildnum = System.getProperty("BUILD_NUMBER");    
            imgpath = "【Jenkinsのworkspace】\\target\\error-img\\BUILD" + buildnum + "\\";
        // for local
        } else {
            Date date = new Date();
            Format fmt = new SimpleDateFormat("yyyyMMdd_HHmmss");
            String time = fmt.format(date);
            imgpath = "C:\\temp\\" + time + "\\";
        }
        File dir = new File(imgpath);

        // フォルダがなかったら作成する
        if (!dir.exists()) {
            dir.mkdir();
        }

    }
}

failedメソッドをオーバーライドして、テストがfailになった時にスクリーンショットを撮るようにします。

// 失敗したときはスクリーンショットを撮る
@Override
protected void failed(Throwable e, Description description) {
    super.failed(e, description);
    String[] classname = description.getClassName().split("\\.");
    try {
        capAll(String.format("%s_%s",
                  classname[classname.length - 1], description.getMethodName()));
    } catch (Exception e2) {
        e2.printStackTrace();
    }
}

スクリーンショットを取得するメソッドはこちら。
エラー等でアラートが出ているとスクリーンショットが撮れないのでアラートを閉じてから撮影するようにしています。

/**
 * screenshotを取得する
 * @param title
 * @throws WebDriverException
 * @throws IOException
 */
public void capture(String title) throws WebDriverException, IOException {

    WebDriverWait wait = new WebDriverWait(driver, 3);
    try {
        Alert alert = wait.until(ExpectedConditions.alertIsPresent());
        alert.accept();
    } catch (TimeoutException e) {
    }

    String path = "";
    path = imgpath + title +".png";

    File file = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    FileUtils.copyFile(file, new File(path));

}

スクロールの発生するような画面で、画面全体のハードコピーを撮りたい場合はこちら。
自動でスクロールして一枚の画像として保存してくれます。
画面全体をスクロールする画面であればCSSセレクタは"body"でOK。
※JQueryを使う前提となっています。

/**
 * screenshotを取得する
 * @param title
 * @throws WebDriverException
 * @throws IOException
 */
public void capAll(String title) throws WebDriverException, IOException {

    driver.switchTo().defaultContent();
    TakesScreenshot ts = (TakesScreenshot) new Augmenter().augment(driver);

    //JS実行用のExecuter
    JavascriptExecutor jexec = (JavascriptExecutor) driver;

    final String SCROLLAREA = "【スクロールエリアのcssセレクタ】";

    // スクロールエリアのある画面
    String isContent = String.valueOf(jexec.executeScript("return $('" + SCROLLAREA + "').length"));
    if("1".equals(isContent)) {

        //画面サイズで必要なものを取得
        int innerH = Integer.parseInt(String.valueOf(jexec.executeScript("return $('" + SCROLLAREA + "').height()")));
        int innerW =Integer.parseInt(String.valueOf(jexec.executeScript("return $('" + SCROLLAREA + "').width()")));
        int scrollH = Integer.parseInt(String.valueOf(jexec.executeScript("return $('" + SCROLLAREA + "')[0].scrollHeight")));
        int scrollW = Integer.parseInt(String.valueOf(jexec.executeScript("return $('" + SCROLLAREA + "')[0].scrollWidth")));
        int windowH = Integer.parseInt(String.valueOf(jexec.executeScript("return window.innerHeight")));
        int windowW = Integer.parseInt(String.valueOf(jexec.executeScript("return window.innerWidth")));
        if(scrollW > innerW) {
            windowH -= 12; // scrollbarの分
            innerH -= 12;
        }
        if(scrollH > innerH) {
            windowW -= 12; // scrollbarの分
            innerW -= 12;
        }

        int headerH = windowH - innerH;
        int headerW = windowW - innerW;
        //イメージを扱うための準備
        BufferedImage img = new BufferedImage(headerW + scrollW, headerH + scrollH, BufferedImage.TYPE_INT_ARGB);
        Graphics g = img.getGraphics();


        int j = 0;
        int scrollableW = scrollW + innerW;
        // 横スクロールのループ
        while(scrollableW > innerW){
            scrollableW -= innerW;
            int scrollableH = scrollH + innerH;
            int i = 0;
            // 縦スクロールのループ
            while(scrollableH > innerH){
                scrollableH -= innerH;
                // スクリーンショットを取得
                BufferedImage imageParts = ImageIO.read(ts.getScreenshotAs(OutputType.FILE));

                if(i == 0 && j == 0) {
                    // 1枚目は画面をそのまま貼る
                    g.drawImage(imageParts, windowW * j, windowH * i, null);
                } else if(scrollableH <= innerH && scrollableW <= innerW) {
                    // 縦、横ともに最後は右下を埋めるように貼り付け
                    g.drawImage(imageParts, headerW + innerW * j, headerH + innerH * i, headerW + innerW * (j + 1), headerH + innerH * (i + 1),
                            windowW-scrollableW, windowH - scrollableH, windowW, windowH, null);
                } else if(scrollableH <= innerH) {
                    // 縦スクロールの最後は下から埋めるように貼り付け
                    g.drawImage(imageParts, headerW + innerW * j, headerH + innerH * i, headerW + innerW * (j + 1), headerH + innerH * (i + 1),
                            headerW, windowH - scrollableH, windowW, windowH, null);
                } else if(scrollableW <= innerW) {
                    // 横スクロール最後は右から埋めるように貼り付け
                    g.drawImage(imageParts, headerW + innerW * j, headerH + innerH * i, headerW + innerW * (j + 1), headerH + innerH * (i + 1),
                            windowW-scrollableW, headerH, windowW, windowH, null);
                } else {
                    g.drawImage(imageParts, windowW * j, windowH * i, null);
                    // 途中の場合はscrollした分だけ貼る
                    //g.drawImage(imageParts, headerW + innerW * j, headerH + innerH * i, headerW + innerW * (j + 1), headerH + innerH * (i + 1),
                    //      headerW, headerH, windowW, windowH, null);
                }

                // 縦に1画面分スクロール
                i++;
                jexec.executeScript("$('" + SCROLLAREA + "').animate({scrollTop:" + innerH * i + "});");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }

            }        

            // 縦スクロールが終わったら1回右にスクロールしてまた縦スクロール
            j++;
            jexec.executeScript("$('" + SCROLLAREA + "').animate({scrollTop: 0, scrollLeft:" + innerW * j + "});");     
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }

        }

        // 画像をファイルに出力する
        String path = imgpath + title +".png";
        ImageIO.write(img, "png", new File(path));

    } else {
        capture(title);
    }

}

TestWatcherクラスが出来上がったら、
テストクラス内で上記で作成したクラスを@Ruleアノテーションをつけてインスタンス化します。

@Rule
public SeleniumTestWatcher watcher = new SeleniumTestWatcher();

すると、JUnitのテスト失敗時に日付_時刻フォルダの下に失敗時のハードコピーが保存されるようになります。

Jenkinsで「ビルド後の処理」の「成果物を保存」に以下のように書いておくとJenkins上から画像を確認できるようになります。

target/error-img/BUILD${BUILD_NUMBER}/*.png

「ビルドの成果物」に保存した画像が表示されるようになります。

クラス全体は一応githubに置いてあります。
https://github.com/widora99/seleniumTest