NỘI DUNG BÀI HỌC
✅Setup code multiple platform và parallel execution (Android and iOS)
✅Run code multiple platform
✅ Tạo source code mới chạy theo kiểu multiple platform
Source GitHub: https://github.com/anhtester/AppiumJava122024_Multi_Platform
- Xoá bỏ hết các nội dung bài cũ
- Chỉnh sửa lại class BaseTest
✅ Setup code multiple platform và parallel execution (Android and iOS)
🔆 Chỉnh sửa lại class BaseTest hỗ trợ Multiple Platform
package com.anhtester.common;
import com.anhtester.drivers.DriverManager;
import com.anhtester.helpers.SystemHelpers;
import com.anhtester.keywords.MobileUI;
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.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Objects;
public class BaseTest {
private AppiumDriverLocalService service;
private String HOST = "127.0.0.1";
private String PORT = "4723";
private int TIMEOUT_SERVICE = 60;
/**
* 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
*/
//@Parameters({"host", "port"})
//@BeforeSuite
public void runAppiumServer(String host, String port) {
System.out.println("host in AppiumServer: " + host);
System.out.println("port in AppiumServer: " + port);
//Set host and port
HOST = host;
PORT = port;
//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.");
}
}
/**
* 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 platformVersion Phiên bản platform
* @param deviceName Tên thiết bị
* @param udid UDID của thiết bị (quan trọng cho parallel)
* @param automationName Tên automation engine (UiAutomator2/XCUITest)
* @param appPackage Package của app Android
* @param appActivity Activity của app Android
* @param noReset Không reset app trước khi chạy
* @param fullReset Reset app trước khi chạy
* @param autoGrantPermissions Tự động cấp quyền cho app
* @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)
* @param MalformedURLException Bẫy lỗi khi tạo URL
*/
@BeforeMethod(alwaysRun = true)
@Parameters({"platformName", "platformVersion", "deviceName", "udid", "automationName", "appPackage", "appActivity", "noReset", "fullReset", "autoGrantPermissions", "host", "port", "bundleId", "wdaLocalPort", "systemPort"})
public void setUpDriver(String platformName, String platformVersion, String deviceName, @Optional String udid, @Optional String automationName, @Optional String appPackage, @Optional String appActivity, boolean noReset, boolean fullReset, boolean autoGrantPermissions, String host, String port, @Optional String bundleId, @Optional String wdaLocalPort, @Optional String systemPort) throws MalformedURLException {
//Khởi động Appium server
runAppiumServer(host, port);
//Print tất cả các thông số
System.out.println("platformName: " + platformName);
System.out.println("platformVersion: " + platformVersion);
System.out.println("deviceName: " + deviceName);
System.out.println("udid: " + udid);
System.out.println("automationName: " + automationName);
System.out.println("appPackage: " + appPackage);
System.out.println("appActivity: " + appActivity);
System.out.println("noReset: " + noReset);
System.out.println("fullReset: " + fullReset);
System.out.println("autoGrantPermissions: " + 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(platformVersion);
options.setDeviceName(deviceName);
if (udid != null && !udid.isEmpty()) {
options.setUdid(udid);
}
if (appPackage != null && !appPackage.isEmpty()) {
options.setAppPackage(appPackage);
}
if (appActivity != null && !appActivity.isEmpty()) {
options.setAppActivity(appActivity);
}
// options.setApp("/path/to/your/app.apk");
options.setAutomationName(Objects.requireNonNullElse(automationName, "UiAutomator2"));
options.setNoReset(noReset);
options.setFullReset(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(platformVersion);
options.setDeviceName(deviceName);
// options.setApp("/path/to/your/app.app or .ipa");
if (bundleId != null && !bundleId.isEmpty()) {
options.setBundleId(bundleId);
}
options.setAutomationName(Objects.requireNonNullElse(automationName, "XCUITest"));
options.setNoReset(false);
options.setFullReset(false);
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);
} 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) {
DriverManager.quitDriver();
System.out.println("##### Driver quit and removed.");
}
stopAppiumServer();
}
//@AfterSuite
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();
}
}
- @Parameters
để truyền tham số từ file XML vào các hàm (thuộc TestNG).
- Các tham số trong hàm phải tương ứng với các tham số bên trên @Parameters thì mới nhận được giá trị.
- Lúc này chúng ta dùng IF ELSE để phân tách ra 2 loại nền tảng Android và iOS thông qua tham số "platformName" được truyền từ XML file.
- Phần @BeforeSuite trước đây thiết lập run Appium Server thì bây giờ chúng ta gộp nó chung với hàm setupDriver của @BeforeMethod luôn. Vì chúng ta cần tách biệt Appium Server cho multi thread có thể chạy parallel.
🔆 Vẫn sử dụng class DriverManager như trước
package com.anhtester.drivers;
import io.appium.java_client.AppiumDriver;
public class DriverManager {
private static ThreadLocal<AppiumDriver> driver = new ThreadLocal<>();
public static void setDriver(AppiumDriver driverInstance) {
driver.set(driverInstance);
}
public static AppiumDriver getDriver() {
return driver.get();
}
public static void closeDriver() {
if (driver.get() != null) {
getDriver().close();
}
}
public static void quitDriver() {
if (driver.get() != null) {
getDriver().quit();
driver.remove();
}
}
}
🔆 Tạo 2 devices mobile khác nhau để chạy
Thầy chạy 2 thiết bị Android, sau này các bạn thay thành Android và iOS nhé.
emulator @Pixel_9_Pro_XL_API_34
emulator @Pixel_8_Pro_API_35_2
🔆 Tạo file suite XML để thiết lập các tham số cho @Parameters
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite Regression Test" verbose="1" parallel="tests">
<test name="Platform Android Test 1 - Taurus App">
<parameter name="platformName" value="Android"/>
<parameter name="platformVersion" value="15"/>
<parameter name="deviceName" value="Pixel_8_Pro_API_35_2"/>
<parameter name="udid" value="emulator-5554"/>
<parameter name="automationName" value="UiAutomator2"/>
<parameter name="appPackage" value="com.anhtester.mobile_app.taurus"/>
<parameter name="appActivity" value="com.anhtester.mobile_app.taurus.MainActivity"/>
<parameter name="noReset" value="false"/>
<parameter name="fullReset" value="false"/>
<parameter name="autoGrantPermissions" value="true"/>
<parameter name="host" value="127.0.0.1"/>
<parameter name="port" value="8000"/>
<parameter name="systemPort" value="8201"/>
<classes>
<class name="com.anhtester.Bai20_Multiple_Platform.testcases.MenuTest"/>
</classes>
</test>
<test name="Platform Android Test 2 - Taurus App">
<parameter name="platformName" value="Android"/>
<parameter name="platformVersion" value="14"/>
<parameter name="deviceName" value="Pixel_9_Pro_XL_API_34"/>
<parameter name="udid" value="emulator-5556"/>
<parameter name="automationName" value="UiAutomator2"/>
<parameter name="appPackage" value="com.anhtester.mobile_app.taurus"/>
<parameter name="appActivity" value="com.anhtester.mobile_app.taurus.MainActivity"/>
<parameter name="noReset" value="false"/>
<parameter name="fullReset" value="false"/>
<parameter name="autoGrantPermissions" value="true"/>
<parameter name="host" value="127.0.0.1"/>
<parameter name="port" value="9000"/>
<parameter name="systemPort" value="8202"/>
<classes>
<class name="com.anhtester.Bai20_Multiple_Platform.testcases.MenuTest"/>
</classes>
</test>
<!-- <test name="Platform iOS Test - Taurus App">-->
<!-- <parameter name="platformName" value="iOS"/>-->
<!-- <parameter name="platformVersion" value="18"/>-->
<!-- <parameter name="deviceName" value="iPhone 14"/>-->
<!-- <parameter name="automationName" value="XCUITest"/>-->
<!-- <parameter name="bundleId" value=""/>-->
<!-- <parameter name="noReset" value="false"/>-->
<!-- <parameter name="fullReset" value="false"/>-->
<!-- <parameter name="autoGrantPermissions" value="true"/>-->
<!-- <parameter name="host" value="127.0.0.1"/>-->
<!-- <parameter name="port" value="6000"/>-->
<!-- <parameter name="wdaLocalPort" value="8101"/>-->
<!-- <classes>-->
<!-- <class name="com.anhtester.Bai20_Multiple_Platform.testcases.MenuTest"/>-->
<!-- </classes>-->
<!-- </test>-->
</suite>
Chỗ thẻ suite có thuộc tính parallel dùng để chạy song song:
-
parallel="tests"
: chạy các module thẻ <test> song song. -
Bạn có thể thay đổi
parallel
thành:-
"tests"
– chạy song song các<test>
trong suite -
"methods"
– chạy song song các method trong class -
"class"
– chạy song song các class
-
Ngoài ra bạn có thể sử dụng:
thread-count="3"
: chạy tối đa 3 thread cùng lúc. Mục đích giới hạn số luồng chạy khi có quá nhiều test cases để không bị treo máy.
⚠️ Lưu ý khi chạy parallel:
-
Đảm bảo test cases độc lập, không share chung resource (ví dụ: WebDriver, AppiumDriver, database state).
-
Nếu dùng Selenium/WebDriver, bạn nên tạo driver riêng cho mỗi thread (ThreadLocal hoặc Factory pattern).
-
Debug khó hơn nếu test chạy không tuần tự – nên ghi log cẩn thận.
✅ Run code multiple platform
Chạy file suite XML và lưu ý là bật trước thiết bị mobile sẵn. Thầy thì demo với 2 thiết bị Android nhé.