NỘI DUNG BÀI HỌC
✅ Record video screen trong Appium Java
✅ Thiết lập gọi hàm sử dụng tại TestListener
Đầu tiên chúng ta cần tạo class chung để lưu trữ các hàm xử lý dành cho Screenshot và Record video luôn.
Cụ thể đặt tên "CaptureHelpers" và đặt trong package helpers (src/main/java/com/anhtester/helpers).
package com.anhtester.helpers;
import com.anhtester.drivers.DriverManager;
import com.anhtester.utils.DateUtils;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidStartScreenRecordingOptions;
import io.appium.java_client.screenrecording.CanRecordScreen;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Base64;
public class CaptureHelpers {
/**
* Hàm static để chụp ảnh màn hình và lưu vào đường dẫn file được chỉ định.
*
* @param fileName Đường dẫn file nơi muốn lưu ảnh chụp màn hình (ví dụ: "screenshots/image.png").
*/
public static void captureScreenshot(String fileName) {
try {
// Ép kiểu driver thành TakesScreenshot để lấy ảnh màn hình
File srcFile = ((TakesScreenshot) DriverManager.getDriver()).getScreenshotAs(OutputType.FILE);
SystemHelpers.createFolder(SystemHelpers.getCurrentDir() + "exports/screenshots");
String filePath = SystemHelpers.getCurrentDir() + "exports/screenshots/" + fileName + "_" + Thread.currentThread().getId() + "_" + SystemHelpers.makeSlug(DateUtils.getCurrentDateTime()) + ".png";
// Tạo đối tượng Path cho file đích
Path targetPath = new File(filePath).toPath();
// Sao chép file từ nguồn sang đích, thay thế file nếu đã tồn tại
Files.copy(srcFile.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Chụp ảnh màn hình thành công, lưu tại: " + targetPath.toAbsolutePath());
} catch (IOException e) {
System.err.println("Lỗi trong quá trình lưu file ảnh: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("Lỗi trong quá trình chụp ảnh màn hình: " + e.getMessage());
e.printStackTrace();
}
}
// Bắt đầu ghi video
public static void startRecording() {
if (DriverManager.getDriver() != null) {
((AndroidDriver) DriverManager.getDriver()).startRecordingScreen(
new AndroidStartScreenRecordingOptions()
.withBitRate(4000000) // default: 4000000
.withVideoSize("1080x2400") // 720 x 1600, 1080 x 2400 pixels
.withTimeLimit(Duration.ofMinutes(10))); // 10 minutes max video length
System.out.println("Bắt đầu ghi video cho " + DriverManager.getDriver().getCapabilities().getCapability("deviceName"));
}
}
// Dừng ghi video và lưu file
public static void stopRecording(String videoFileName) {
if (DriverManager.getDriver() != null) {
try {
String base64Video = ((CanRecordScreen) ((AndroidDriver) DriverManager.getDriver())).stopRecordingScreen();
System.out.println("Base64 video length: " + (base64Video != null ? base64Video.length() : "null"));
if (base64Video != null && !base64Video.isEmpty()) {
byte[] videoBytes = Base64.getDecoder().decode(base64Video);
System.out.println("Video bytes length: " + videoBytes.length);
File videoFile = new File(videoFileName);
try (FileOutputStream fos = new FileOutputStream(videoFile)) {
fos.write(videoBytes);
}
System.out.println("Video được lưu tại: " + videoFile.getAbsolutePath() + " (Size: " + videoFile.length() + " bytes)");
} else {
System.out.println("Không có dữ liệu video để lưu.");
}
} catch (Exception e) {
System.err.println("Lỗi khi dừng ghi video: " + e.getMessage());
}
}
}
}
✅ Screenshot trong Appium Java
Chúng ta sử dụng hàm captureScreenshot() như khai báo bên trên. Mỗi lần gọi dùng đặt tên file hình ảnh cần lưu trữ.
Ví dụ:
package com.anhtester.Bai25_Screenshot_RecordVideo.testcases;
import com.anhtester.Bai25_Screenshot_RecordVideo.pages.LoginPage;
import com.anhtester.common.BaseTest_Json_Device;
import com.anhtester.helpers.CaptureHelpers;
import org.testng.annotations.Test;
public class LoginTest extends BaseTest_Json_Device {
private LoginPage loginPage;
@Test
public void testLoginSuccess() {
loginPage = new LoginPage();
loginPage.login("admin", "admin");
CaptureHelpers.captureScreenshot("testLoginSuccess");
loginPage.verifyLoginSuccess();
}
@Test
public void testLoginFailWithUsernameInvalid() {
loginPage = new LoginPage();
loginPage.login("admin123", "admin");
CaptureHelpers.captureScreenshot("testLoginFailWithUsernameInvalid");
loginPage.verifyLoginFail();
}
}
✅ Record video screen trong Appium Java
Để bắt đầu record video chúng ta gọi hàm startRecording() tại vị trí hàm createDriver bên class BaseTest đang dùng và khi kết thúc test case chúng ta gọi hàm stopRecording(String videoFileName) và truyền đường dẫn cùng tên file để lưu trữ nội dung video, gọi tại hàm tearDownDriver đang dùng bên BaseTest.
File xuất ra có định dạng là mp4.
Ví dụ:
package com.anhtester.common;
import com.anhtester.constants.ConfigData;
import com.anhtester.drivers.DriverManager;
import com.anhtester.helpers.CaptureHelpers;
import com.anhtester.helpers.SystemHelpers;
import com.anhtester.keywords.MobileUI;
import com.anhtester.listeners.TestListener;
import com.anhtester.utils.DateUtils;
import io.appium.java_client.AppiumBy;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import io.appium.java_client.service.local.flags.GeneralServerFlag;
import org.testng.annotations.*;
import java.net.URL;
import java.time.Duration;
import java.util.Objects;
@Listeners({TestListener.class})
public class BaseTest_Json_Device {
private AppiumDriverLocalService service;
private String HOST = "127.0.0.1";
private String PORT = "4723";
private int TIMEOUT_SERVICE = 60;
private String videoFileName;
/**
* Chạy Appium server với host và port được chỉ định.
*
* @param host Địa chỉ host của Appium server
* @param port Port của Appium server
*/
public void runAppiumServer(String host, String port) {
System.out.println("HOST: " + host);
System.out.println("PORT: " + port);
//Set host and port
if (host == null || host.isEmpty()) {
host = HOST;
} else {
HOST = host;
}
if (port == null || port.isEmpty()) {
port = PORT;
} else {
PORT = port;
}
TIMEOUT_SERVICE = Integer.parseInt(ConfigData.TIMEOUT_SERVICE);
//Kill process on port
SystemHelpers.killProcessOnPort(PORT);
//Build the Appium service
AppiumServiceBuilder builder = new AppiumServiceBuilder();
builder.withIPAddress(HOST);
builder.usingPort(Integer.parseInt(PORT));
builder.withArgument(GeneralServerFlag.LOG_LEVEL, "info"); // Set log level (optional)
builder.withTimeout(Duration.ofSeconds(TIMEOUT_SERVICE));
//Start the server with the builder
service = AppiumDriverLocalService.buildService(builder);
service.start();
if (service.isRunning()) {
System.out.println("##### Appium server started on " + HOST + ":" + PORT);
} else {
System.out.println("Failed to start Appium server on LOCAL.");
}
}
/**
* Thiết lập (khởi tạo và lưu trữ) AppiumDriver cho luồng hiện tại.
*
* @param platformName Tên platform (Android/iOS)
* @param deviceName Tên thiết bị trong device.json
* @param udid UDID của thiết bị Android (quan trọng cho parallel)
* @param host Địa chỉ host của Appium server
* @param port Port của Appium server
* @param bundleId Bundle ID của app iOS
* @param wdaLocalPort Port WDA (iOS parallel)
* @param systemPort Port System (Android parallel)
*/
@BeforeMethod(alwaysRun = true)
@Parameters({"platformName", "deviceName", "udid", "host", "port", "bundleId", "wdaLocalPort", "systemPort"})
public void setUpDriver(String platformName, String deviceName, @Optional String udid, String host, String port, @Optional String bundleId, @Optional String wdaLocalPort, @Optional String systemPort) {
//Khởi động Appium server dưới máy local
if (ConfigData.APPIUM_DRIVER_LOCAL_SERVICE.trim().equalsIgnoreCase("true")) {
System.out.println("Khởi động Appium server LOCAL: " + host + ":" + port);
runAppiumServer(host, port);
} else {
System.out.println("Chạy Appium server từ xa hoặc đã bật sẵn.");
}
//Print tất cả các thông số
System.out.println("platformName: " + platformName);
System.out.println("platformVersion: " + ConfigData.getValueJsonConfig(platformName, deviceName, "platformVersion"));
System.out.println("deviceName: " + ConfigData.getValueJsonConfig(platformName, deviceName, "deviceName"));
System.out.println("udid: " + ConfigData.getValueJsonConfig(platformName, deviceName, "udid"));
System.out.println("automationName: " + ConfigData.getValueJsonConfig(platformName, deviceName, "automationName"));
System.out.println("appPackage: " + ConfigData.getValueJsonConfig(platformName, deviceName, "appPackage"));
System.out.println("appActivity: " + ConfigData.getValueJsonConfig(platformName, deviceName, "appActivity"));
System.out.println("noReset: " + ConfigData.getValueJsonConfig(platformName, deviceName, "noReset"));
System.out.println("fullReset: " + ConfigData.getValueJsonConfig(platformName, deviceName, "fullReset"));
System.out.println("autoGrantPermissions: " + ConfigData.getValueJsonConfig(platformName, deviceName, "autoGrantPermissions"));
System.out.println("host: " + host);
System.out.println("port: " + port);
System.out.println("bundleId: " + bundleId);
System.out.println("wdaLocalPort: " + wdaLocalPort);
System.out.println("systemPort: " + systemPort);
AppiumDriver driver = null;
try {
if (platformName.equalsIgnoreCase("Android")) {
UiAutomator2Options options = new UiAutomator2Options();
options.setPlatformName(platformName);
options.setPlatformVersion(ConfigData.getValueJsonConfig(platformName, deviceName, "platformVersion"));
options.setDeviceName(ConfigData.getValueJsonConfig(platformName, deviceName, "deviceName"));
if (udid != null && !udid.isEmpty()) {
options.setUdid(udid);
}
String appPackage = ConfigData.getValueJsonConfig(platformName, deviceName, "appPackage");
if (appPackage != null && !appPackage.isEmpty()) {
options.setAppPackage(appPackage);
}
String appActivity = ConfigData.getValueJsonConfig(platformName, deviceName, "appActivity");
if (appActivity != null && !appActivity.isEmpty()) {
options.setAppActivity(appActivity);
}
// options.setApp("/path/to/your/app.apk");
options.setAutomationName(Objects.requireNonNullElse(ConfigData.getValueJsonConfig(platformName, deviceName, "automationName"), "UiAutomator2"));
options.setNoReset(Boolean.parseBoolean(ConfigData.getValueJsonConfig(platformName, deviceName, "noReset")));
options.setFullReset(Boolean.parseBoolean(ConfigData.getValueJsonConfig(platformName, deviceName, "fullReset")));
if (systemPort != null && !systemPort.isEmpty()) {
options.setSystemPort(Integer.parseInt(systemPort));
}
driver = new AndroidDriver(new URL("http://" + host + ":" + port), options);
System.out.println("Khởi tạo AndroidDriver cho thread: " + Thread.currentThread().getId() + " trên thiết bị: " + deviceName);
} else if (platformName.equalsIgnoreCase("iOS")) {
XCUITestOptions options = new XCUITestOptions();
options.setPlatformName(platformName);
options.setPlatformVersion(ConfigData.getValueJsonConfig(platformName, deviceName, "platformVersion"));
options.setDeviceName(ConfigData.getValueJsonConfig(platformName, deviceName, "deviceName"));
// options.setApp("/path/to/your/app.app or .ipa");
if (bundleId != null && !bundleId.isEmpty()) {
options.setBundleId(bundleId);
}
options.setAutomationName(Objects.requireNonNullElse(ConfigData.getValueJsonConfig(platformName, deviceName, "automationName"), "XCUITest"));
options.setNoReset(Boolean.parseBoolean(ConfigData.getValueJsonConfig(platformName, deviceName, "noReset")));
options.setFullReset(Boolean.parseBoolean(ConfigData.getValueJsonConfig(platformName, deviceName, "fullReset")));
if (wdaLocalPort != null && !wdaLocalPort.isEmpty()) {
options.setWdaLocalPort(Integer.parseInt(wdaLocalPort));
}
// options.setXcodeOrgId("YOUR_TEAM_ID");
// options.setXcodeSigningId("iPhone Developer");
driver = new IOSDriver(new URL("http://" + host + ":" + port), options);
System.out.println("Khởi tạo IOSDriver cho thread: " + Thread.currentThread().getId() + " trên thiết bị: " + deviceName);
} else {
throw new IllegalArgumentException("Platform không hợp lệ: " + platformName);
}
// Lưu driver vào ThreadLocal
DriverManager.setDriver(driver);
// Tạo tên file video duy nhất dựa trên device và thread
SystemHelpers.createFolder(SystemHelpers.getCurrentDir() + "exports/videos");
videoFileName = SystemHelpers.getCurrentDir() + "exports/videos/recording_" + deviceName + "_" + Thread.currentThread().getId() + "_" + SystemHelpers.makeSlug(DateUtils.getCurrentDateTime()) + ".mp4";
CaptureHelpers.startRecording();
} catch (Exception e) {
System.err.println("❌Lỗi nghiêm trọng khi khởi tạo driver cho thread " + Thread.currentThread().getId() + " trên device " + deviceName + ": " + e.getMessage());
// Có thể ném lại lỗi để TestNG biết test setup thất bại
throw new RuntimeException("❌Không thể khởi tạo Appium driver ", e);
}
}
@AfterMethod(alwaysRun = true)
public void tearDownDriver() {
if (DriverManager.getDriver() != null) {
//Stop recording video
MobileUI.sleep(2);
CaptureHelpers.stopRecording(videoFileName);
DriverManager.quitDriver();
System.out.println("##### Driver quit and removed.");
}
//Dừng Appium server LOCAL nếu đã khởi động
if (ConfigData.APPIUM_DRIVER_LOCAL_SERVICE.trim().equalsIgnoreCase("true")) {
stopAppiumServer();
}
}
/**
* Stop Appium server.
*/
public void stopAppiumServer() {
if (service != null && service.isRunning()) {
service.stop();
System.out.println("##### Appium server stopped on " + HOST + ":" + PORT);
}
//Kill process on port
SystemHelpers.killProcessOnPort(PORT);
}
/**
* Tải xuống dữ liệu từ server. Chỉ dành cho Taurus App.
*
* @param dataNumber Số thứ tự của dữ liệu cần tải xuống
*/
public void downloadDataFromServer(int dataNumber) {
//Navigate to config to download database demo
DriverManager.getDriver().findElement(AppiumBy.accessibilityId("Config")).click();
DriverManager.getDriver().findElement(AppiumBy.accessibilityId("Server database")).click();
MobileUI.sleep(2);
DriverManager.getDriver().findElement(AppiumBy.xpath("//android.view.View[contains(@content-desc,'Data " + dataNumber + "')]/android.widget.Button")).click();
DriverManager.getDriver().findElement(AppiumBy.accessibilityId("Replace")).click();
MobileUI.sleep(1);
//Handle Alert Message, check displayed hoặc getText/getAttribute để kiểm tra nội dung message
if (DriverManager.getDriver().findElement(AppiumBy.accessibilityId("Downloaded")).isDisplayed()) {
System.out.println("Database demo downloaded.");
} else {
System.out.println("Warning!! Can not download Database demo.");
}
MobileUI.sleep(2);
DriverManager.getDriver().findElement(AppiumBy.accessibilityId("Back")).click();
}
}
Khi đó bất kỳ class nào kế thừa class BaseTest có khai báo record video sẽ tự động record và lưu trữ vào đường dẫn như đã khai báo.
Ví dụ chạy lại class LoginTest trên vừa Screenshot vừa Record video screen.
✅ Thiết lập gọi hàm sử dụng tại TestListener
Kế thừa từ nội dung bài trước TestListener trong TestNG Framework, chúng ta gọi các hàm screenshot và record video tại các hàm sự kiện onTestSuccess() và onTestFailure().
package com.anhtester.listeners;
import com.anhtester.helpers.CaptureHelpers;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TestListener implements ITestListener {
@Override
public void onStart(ITestContext result) {
System.out.println("♻\uFE0F Setup môi trường: " + result.getStartDate());
}
@Override
public void onFinish(ITestContext result) {
System.out.println("\uD83D\uDD06 Kết thúc chạy test: " + result.getEndDate());
}
@Override
public void onTestStart(ITestResult result) {
System.out.println("➡\uFE0F Bắt đầu chạy test case: " + result.getName());
}
@Override
public void onTestSuccess(ITestResult result) {
System.out.println("✅ Test case " + result.getName() + " is passed.");
LocalDateTime now = LocalDateTime.now(); // lấy ngày giờ hiện tại
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDate = now.format(formatter);
System.out.println("Thời gian: " + formattedDate);
CaptureHelpers.captureScreenshot(result.getName());
}
@Override
public void onTestFailure(ITestResult result) {
System.out.println("❌ Test case " + result.getName() + " is failed.");
LocalDateTime now = LocalDateTime.now(); // lấy ngày giờ hiện tại
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDate = now.format(formatter);
System.out.println("Thời gian: " + formattedDate);
System.out.println("Nguyên nhân: " + result.getThrowable());
CaptureHelpers.captureScreenshot(result.getName());
//Connect Jira
//Create new issue on Jira
//Ghi logs vào file
//Xuất report html nhìn trực quan và đẹp mắt
}
@Override
public void onTestSkipped(ITestResult result) {
System.out.println("⛔\uFE0F Test case " + result.getName() + " is skipped.");
}
}
Khi ấy chúng ta không cần gọi hàm chụp ảnh màn hình ở từng test cases nữa, mà chỉ cần chỉ định chung chụp ảnh khi success hoặc failed. Khâu đặt tên hình ảnh cũng tiện lợi nhanh gọn lẹ.