NỘI DUNG BÀI HỌC

✅Tạo source code mới chạy theo kiểu multiple platform
✅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 AndroidiOS 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 AndroidiOS 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é.

[Appium Java] Bài 20 - Chạy test trên nhiều loại thiết bị mobile (Android and iOS) | Anh Tester

Teacher

Teacher

Anh Tester

Software Quality Engineer

Đường dẫu khó chân vẫn cần bước đi
Đời dẫu khổ tâm vẫn cần nghĩ thấu

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học