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);
	                }

	                // 縦に１画面分スクロール
	                i++;
	                jexec.executeScript("$('" + SCROLLAREA + "').animate({scrollTop:" + innerH * i + "});");
	                try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
					}
	                
	            }        
	            
	            // 縦スクロールが終わったら１回右にスクロールしてまた縦スクロール
	            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のテスト失敗時に日付_時刻フォルダの下に失敗時のハードコピーが保存されるようになります。
![WS000016.jpg](https://qiita-image-store.s3.amazonaws.com/0/159389/d2424bcc-eace-b039-cc69-0e4fb4fbc133.jpeg)


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

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

![WS000017.jpg](https://qiita-image-store.s3.amazonaws.com/0/159389/54fa4e05-4a45-031d-7a82-81e73942315e.jpeg)

「ビルドの成果物」に保存した画像が表示されるようになります。
![WS000018.jpg](https://qiita-image-store.s3.amazonaws.com/0/159389/8923bd64-07d4-97fb-5832-4ad6394fe07b.jpeg)


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