Seleniumのテストケースにスクリーン録画の機能を追加する
Selenium + Junitでのオートパイロットを利用していろいろな操作を自動化したり、Webアプリの納品前テストなどに利用したりと最近利用機会が増えているのですが、特に他人の作ったサービスの操作をSeleniumで自動化している場合にはページレイアウトの変更やtypoの修正などのちょっとした変更があっただけでエレメントの特定などができなくなってしまい、自動操作が続かなくなることも多々あります。
そんな時は大抵はもう一度目視で画面を監視しながら同じ操作を行わせ、どこで不具合が発生しているのかを確認するわけですが、場合によっては、それは稀にしか起きない現象だったり、再現に時間がかかるものだったりと、「実際に現象が起きた時のスクリーンの状態を録画できたらべんり出来たら便利だろうな」と思っていたらQiitaで次の記事を見つけました。
で、実際にこの記事にかかれている手法を取り入れてみたら思った以上に修正作業の効率アップを図れたので自分のコードにスクリーン録画機能を組み込む際の手順を纏めてみました。
スクリーンの録画に必要なクラスをインポート
import java.io.File;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import org.junit.rules.TestName;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.monte.media.Format;
import org.monte.media.math.Rational;
import static org.monte.media.FormatKeys.*;
import static org.monte.media.VideoFormatKeys.*;
スクリーン録画用のメソッドとルールを定義
2行目のFile file = new File("record");
の部分で録画ファイルを格納するフォルダを指定できます。
14行目からのメソッドScreenRecorder
へと与えるパラメータのEncodingKey
とCompressorNameKey
でMonte Media Libraryによる録画の際のエンコーディングとコンプレッサー(圧縮方式)を指定できます。
Monte Media Libraryでは以下の参考URLにあるようなエンコーディングとコンプレッサーが使える様ですが、どの組み合わせが有効なのかについての記述が見当たらなかったので、その辺りは試行錯誤です。
自分が試行錯誤した結果ではエンコーディングとコンプレッサー共にサンプル中で指定しているTechSmith Screen Capture Codec(ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE
)を使った場合が最も録画ファイルのサイズが小さくなったのですが、ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE
で録画されたファイルの再生には専用のプレーヤーかQuickTime Player用の追加コーデックのインストールが必要となります。
※macOSの場合、このぺーじのEnSharpen decoder
の中から自分の環境に合わせたパッケージをインストールすればTechSmith Screen Capture Codec
の録画ファイルの再生が可能です。
QuickTime Player用追加コーデック(TechSmith Screen Capture Codec)
https://www.techsmith.com/codecs.html
Monte Media LibraryのJavaDocページ
http://www.randelshofer.ch/monte/javadoc/index.html
エンコーディングにENCODING_QUICKTIME_CINEPAK
をコンプレッサーにCOMPRESSOR_NAME_QUICKTIME_CINEPAK
を指定した場合にはmacOSのQuickTime Playerでそのまま再生が可能な状態で録画ファイルが作られるのですが、TechSmith Screen Capture Codecを使用した場合に比べてファイルのサイズはかなり大きくなるのでエンコーディングやコンプレッサーの指定は適材適所で工夫してみてください。
public void startRecording(String name) throws Exception {
File file = new File("record");
Dimension screenSize = driver.manage().window().getSize();
int width = screenSize.width;
int height = screenSize.height;
java.awt.Rectangle captureSize = new java.awt.Rectangle(0,0, width, height);
GraphicsConfiguration gc = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration();
this.screenRecorder = new ScreenRecorder(gc, captureSize,
new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME),
new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
CompressorNameKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
DepthKey, 24, FrameRateKey, Rational.valueOf(15),
QualityKey, 1.0f,
KeyFrameIntervalKey, 15 * 60),
new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, "black",
FrameRateKey, Rational.valueOf(30)),
null, file, name);
this.screenRecorder.start();
}
public void stopRecording() throws Exception {
this.screenRecorder.stop();
}
private ScreenRecorder screenRecorder;
@Rule
public TestName testName = new TestName();
@Rule
public TestWatcher watchman = new TestWatcher() {
@Override
protected void succeeded(Description d) {
System.out.println("I am succeeded() method. name -> " + d.getMethodName());
// テストに成功しても録画ファイルを残すなら次の1行をコメントアウト
screenRecorder.getCreatedMovieFiles().stream().filter(a -> a.toPath().toString().contains(testName.getMethodName())).forEach(a -> a.deleteOnExit());
}
@Override
protected void failed(Throwable th, Description d) {
System.out.println("I am failed() method. name -> " + d.getMethodName());
System.out.println(th.toString());
}
};
初期化処理にスクリーン録画の開始メソッド呼び出しを追加
@Before
public void setUp() throws Exception {
driver = new FirefoxDriver();
baseUrl = "https://console.firebase.google.com";
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
startRecording(testName.getMethodName());
}
@After
public void tearDown() throws Exception {
// テスト終了後すぐに録画を止めると結果が最後まで録画されないことがあるのでスリープ
Thread.sleep(5000);
stopRecording();
driver.quit();
String verificationErrorString = verificationErrors.toString();
if (!"".equals(verificationErrorString)) {
fail(verificationErrorString);
}
}
mavenのプロジェクトファイル(pom.xml)にMonte Media Libraryのrepositoryとdependencyを追加
<repositories>
...
<repository>
<id>additional-repo1</id>
<name>Alfresco Public Repository</name>
<url>https://artifacts.alfresco.com/nexus/content/repositories/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
...
</dependency>
<dependency>
<groupId>org.monte</groupId>
<artifactId>monte-screen-recorder</artifactId>
<version>0.7.7</version>
</dependency>
</dependencies>
スクリーンショット用のクラスを追加
Monte Media LibraryのScreenRecorderを継承して、createMovieFileをOverrideすることで録画ファイルを出力する場所とファイル名をmovieFolderで指定されたフォルダ以下のname + yyyy-MM-dd HH.mm.ss(日付と時刻)へと変更しています。
package net.hoge2.test;
import java.awt.AWTException;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.monte.media.Format;
import org.monte.media.Registry;
public class ScreenRecorder extends org.monte.screenrecorder.ScreenRecorder {
private String name;
public ScreenRecorder(GraphicsConfiguration cfg,
Rectangle captureArea, Format fileFormat, Format screenFormat,
Format mouseFormat, Format audioFormat, File movieFolder,
String name) throws IOException, AWTException {
super(cfg, captureArea, fileFormat, screenFormat, mouseFormat,
audioFormat, movieFolder);
this.name = name;
}
@Override
protected File createMovieFile(Format fileFormat) throws IOException {
if (!movieFolder.exists()) {
movieFolder.mkdirs();
}
else if (!movieFolder.isDirectory()) {
throw new IOException("\"" + movieFolder + "\" is not a directory.");
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH.mm.ss");
return new File(movieFolder,
name + "-" + dateFormat.format(new Date()) + "."
+ Registry.getInstance().getExtension(fileFormat));
}
}
Seleniumでのテストケースの実行中にスクリーンを録画するのにMonte Media Libraryを使っていますが、このライブラリはリモートでsshログインなどを行なった状態でテストケースの実行を行なおうとするとエラーになるので、リモートログインしての実行が必要な場合には関連する箇所をコメントアウトする必要があります。