背景

在工作中会写一些UI自动化测试的case来测试, 但是UI测试的时候, case会失败, 此时如果有个截图或者录屏就很有用. 由于在工作中使用的是cucumber, 所以代码是在cucumber中实现的, 但是作为工具, 也可以独立使用.

方案

在UI case中, 如果给失败的case进行录屏, 那么可以更加有效, 有针对性地对case的失败行为进行分析. 能清除地知道,在执行某些步骤的时候, 失败了.

因此方案如下:

  1. 对每一个case进行录屏
  2. 将失败的case的录屏嵌入到cucumber的报告中.
  3. 展示失败case的视频, 分析问题.

代码

在此我使用的是monte-screen-recorder这个工具, 因为此工具的录屏是avi, 无法直接将视频在html页面上渲染, 所以我使用ffmpeg的java工具包, 将avi转码成mp4.
gradle的依赖是

1
2
3
compile group: 'com.github.stephenc.monte', name: 'monte-screen-recorder', version: '0.7.7.0'
compile group: 'net.masterthought', name: 'cucumber-reporting', version: '4.10.0'
compile group: 'ws.schild', name: 'jave-all-deps', version: '3.0.1'

直接贴代码了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class ScreenRecorderUtil extends ScreenRecorder {
private static ScreenRecorder screenRecorder;

private String scenarioName;
private static String videoName;
private static final String RECORDING_PATH = "./build/recording";

public DPMScreenRecorderUtil(GraphicsConfiguration cfg, Rectangle captureArea, Format fileFormat,
Format screenFormat, Format mouseFormat, Format audioFormat, File movieFolder, String scenarioName)
throws IOException, AWTException {
super(cfg, captureArea, fileFormat, screenFormat, mouseFormat, audioFormat, movieFolder);
this.scenarioName = scenarioName;
}

@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");
videoName = scenarioName + "-" + dateFormat.format(new Date()) + "." + Registry.getInstance().getExtension(fileFormat);
return new File(movieFolder,
scenarioName + "-" + dateFormat.format(new Date()) + "." + Registry.getInstance().getExtension(fileFormat));
}

public static void startRecord(String scenario) throws Exception {

File file = new File(RECORDING_PATH);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int width = screenSize.width;
int height = screenSize.height;

Rectangle captureSize = new Rectangle(0, 0, width, height);

GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice()
.getDefaultConfiguration();
screenRecorder = new DPMScreenRecorderUtil(gc, captureSize,
new Format(MediaTypeKey, FormatKeys.MediaType.FILE, MimeTypeKey, MIME_AVI),
new Format(MediaTypeKey, FormatKeys.MediaType.VIDEO, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
CompressorNameKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
DepthKey, (int) 24, FrameRateKey, Rational.valueOf(15),
QualityKey, 1.0f,
KeyFrameIntervalKey, (int) (15 * 60)),
new Format(MediaTypeKey, FormatKeys.MediaType.VIDEO, EncodingKey, "black", FrameRateKey, Rational.valueOf(30)),
null, file, scenario);
screenRecorder.start();
}

public static void stopRecord() throws Exception {
screenRecorder.stop();
}

public static byte[] getRecord() throws Exception {
return FileUtil.readAsByteArray(convertAviToMP4(new File(RECORDING_PATH + "/" + videoName)));
}

private static File convertAviToMP4(File source) throws Exception {
File target = new File(RECORDING_PATH + "/" + "target.mp4");
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
audio.setBitRate(64000);
audio.setChannels(1);
audio.setSamplingRate(22050);
VideoAttributes video = new VideoAttributes();
video.setCodec("libx264"); //编码设置
video.setBitRate(1920 * 1080); //比特率设置
video.setFrameRate(120);
video.setPixelFormat("yuv420p");
EncodingAttributes attrs = new EncodingAttributes();
attrs.setOutputFormat("mp4");
attrs.setInputFormat("avi");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
Encoder encoder = new Encoder();
MultimediaObject multimediaObject = new MultimediaObject(source);
encoder.encode(multimediaObject, target, attrs);
return target;
}
}

cucumber的case中可以使用hook类, 将case的视频嵌入的报告中, 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CucumberHooks {

@Before(order = Integer.MAX_VALUE)
public void startRecording(Scenario scenario) throws Exception {
if (Boolean.parseBoolean(System.getProperty("recordVideo"))) {
// start recording
ScreenRecorderUtil.startRecord(scenario.getName());
}
}

@After(order = Integer.MAX_VALUE)
public void getRecording(Scenario scenario) throws Exception {
DPMScreenRecorderUtil.stopRecord();
if (scenario.isFailed()) {
scenario.embed(DPMScreenRecorderUtil.getRecord(), "video/mp4");
}
}
}