Having Issue with Threadlocal - if one test finishes earlier i.e iOS closes other thread as well Android

Hi Community,

I hope I find some solution for this issue here, so far I have tried everything I know but still facing the same thing.I’m using appium java-client 7.6.0, testnt 7.6.1, Appium v1.22.3 with 1 ios simulator & 1 android emulator.

Issue:
What is happening is if I run only 2 tests same test file for android & ios everything seems to be working fine, but when I add more test & if one on the test finishes earlier it somehow intervene with other running test as well & closes it(when spinning up new driver session), I have used the thread-local in the Base-test class but still facing the issue.

I’m sharing my Basetest class file,TestNg.xml & one test class.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Mobile Test Automation Suite" parallel="tests" thread-count="2">
    <listeners>
        <listener class-name="com.qa.listeners.testListener"/>
    </listeners>
    <test name="iOS e2e Registration Test">
        <parameter name="platformName" value="iOS"/>
        <parameter name="deviceName" value="iphone12"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="15.5"></parameter>
        <parameter name="udid" value="55896861-DB90-4F13-955A-F65D20F409B3"></parameter>
        <parameter name="wdaLocalPort" value="8100"></parameter>
        <classes>
            <class name="com.qa.tests.E2ERegistrationTest"/>
        </classes>
    </test>
    <test name="Android e2e Registration Test" >
        <parameter name="platformName" value="Android"/>
        <parameter name="deviceName" value="Pixel_3"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="12.0"></parameter>
        <parameter name="udid" value="emulator-5554"></parameter>
        <parameter name="systemPort" value="8200"></parameter>
        <parameter name="mjpegServerPort" value="9000"></parameter>

        <classes>
            <class name="com.qa.tests.E2ERegistrationTest"/>
        </classes>
    </test>
    <test name="iOS login Test">
        <parameter name="platformName" value="iOS"/>
        <parameter name="deviceName" value="iphone12"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="15.5"></parameter>
        <parameter name="udid" value="55896861-DB90-4F13-955A-F65D20F409B3"></parameter>
        <parameter name="wdaLocalPort" value="8101"></parameter>
        <classes>
            <class name="com.qa.tests.LoginTest"/>
        </classes>
    </test>
    <test name="Android login Test">
        <parameter name="platformName" value="Android"/>
        <parameter name="deviceName" value="Pixel_3"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="12.0"></parameter>
        <parameter name="udid" value="emulator-5554"></parameter>
        <parameter name="systemPort" value="8201"></parameter>
        <parameter name="mjpegServerPort" value="9001"></parameter>
        <classes>
            <class name="com.qa.tests.LoginTest"/>
        </classes>
    </test>
    <test name="iOS Logout Test">
        <parameter name="platformName" value="iOS"/>
        <parameter name="deviceName" value="iphone12"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="15.5"></parameter>
        <parameter name="udid" value="55896861-DB90-4F13-955A-F65D20F409B3"></parameter>
        <parameter name="wdaLocalPort" value="8102"></parameter>
        <classes>
            <class name="com.qa.tests.LogoutTest"/>
        </classes>
    </test>
    <test name="Android Logout Test">
        <parameter name="platformName" value="Android"/>
        <parameter name="deviceName" value="Pixel_3"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="12.0"></parameter>
        <parameter name="udid" value="emulator-5554"></parameter>
        <parameter name="systemPort" value="8202"></parameter>
        <parameter name="mjpegServerPort" value="9002"></parameter>
        <classes>
            <class name="com.qa.tests.LogoutTest"/>
        </classes>
    </test>
    <test name="iOS Today Screen Cards Test">
        <parameter name="platformName" value="iOS"/>
        <parameter name="deviceName" value="iphone12"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="15.5"></parameter>
        <parameter name="udid" value="55896861-DB90-4F13-955A-F65D20F409B3"></parameter>
        <parameter name="wdaLocalPort" value="8103"></parameter>
        <classes>
            <class name="com.qa.tests.TodayScreenCardsTest"/>
        </classes>
    </test>
    <test name="Android Today Screen Cards Test">
        <parameter name="platformName" value="Android"/>
        <parameter name="deviceName" value="Pixel_3"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="12.0"></parameter>
        <parameter name="udid" value="emulator-5554"></parameter>
        <parameter name="systemPort" value="8203"></parameter>
        <parameter name="mjpegServerPort" value="9003"></parameter>
        <classes>
            <class name="com.qa.tests.TodayScreenCardsTest"/>
        </classes>
    </test>
</suite>
    public class BaseTest {
    protected static ThreadLocal<AppiumDriver> driver = new ThreadLocal<AppiumDriver>();
    protected static ThreadLocal<Properties> prop = new ThreadLocal<Properties>();
    protected static ThreadLocal<HashMap<String, String>> strings = new ThreadLocal<HashMap<String, String>>();
    protected static ThreadLocal<String> platform = new ThreadLocal<String>();

    public Testutils utils;

    public AppiumDriver getDriver() {
        return driver.get();
    }

    public void setDriver(AppiumDriver driver2) {
        driver.set(driver2);
    }

    public Properties getProperties() {
        return prop.get();
    }

    public void setProperties(Properties prop2) {
        prop.set(prop2);
    }

    public HashMap<String, String> getStrings() {
        return strings.get();
    }

    public void setStrings(HashMap<String, String> strings2) {
        strings.set(strings2);
    }

    public String getPlatform() {
        return platform.get();
    }

    public void setPlatform(String Platform2) {
        platform.set(Platform2);
    }

    public BaseTest() {
        PageFactory.initElements(new AppiumFieldDecorator(getDriver(), Duration.ofSeconds(Testutils.WAIT)), this);

    }


    void setAllureEnvironment(String ios, String android) {
        allureEnvironmentWriter(
                ImmutableMap.<String, String>builder()
                        .put("iOS App Version:", ios)
                        .put("Android App Version:", android)
                        .put("Test Environment:", "")
                        .build());
    }

    @Parameters({"platformName", "deviceName", "emulator", "platformVersion", "udid","wdaLocalPort","systemPort","mjpegServerPort"})
    @BeforeTest
    public void driverInitialize(String platformName, String deviceName, String emulator, String platformVersion, String udid,@Optional("iOS Only") String wdaLocalPort,@Optional("Android Only") String systemPort,@Optional("Android Only")String mjpegServerPort) throws Exception {
        setPlatform(platformName);
        URL url;
        InputStream inputStream = null;
        InputStream stringsis = null;
        Properties prop = new Properties();
        AppiumDriver driver;
        try {

            String propFileName = "config.properties";
            String xmlFilename = "Strings/Strings.xml";
            inputStream = getClass().getClassLoader().getResourceAsStream(propFileName);
            prop.load(inputStream);
            setProperties(prop);
            stringsis = getClass().getClassLoader().getResourceAsStream(xmlFilename);
            utils = new Testutils();
            setStrings(utils.parseStringXML(stringsis));

            DesiredCapabilities caps = new DesiredCapabilities();
            caps.setCapability(MobileCapabilityType.PLATFORM_NAME, platformName);
            caps.setCapability(MobileCapabilityType.DEVICE_NAME, deviceName);
            url = new URL(prop.getProperty("AppiumURL"));


            switch (platformName) {
                case "Android":
                    caps.setCapability(MobileCapabilityType.AUTOMATION_NAME, prop.getProperty("androidAutomationName"));
                    caps.setCapability("appPackage", prop.getProperty("androidAppPackage"));
                    caps.setCapability("appActivity", prop.getProperty("androidAppActivity"));
                    caps.setCapability("appWaitActivity","*");
                    String androidappURL = getClass().getResource(prop.getProperty("androidAppLocation")).getFile();
                    caps.setCapability(MobileCapabilityType.APP, androidappURL);
                    caps.setCapability("systemPort",systemPort);
                    caps.setCapability("mjpegServerPort",mjpegServerPort);
                    if (emulator.equalsIgnoreCase("true")) {
                        caps.setCapability("avd", deviceName);
                        caps.setCapability(MobileCapabilityType.PLATFORM_VERSION, platformVersion);
                    } else {
                        caps.setCapability("udid", udid);
                    }
                    driver = new AndroidDriver(url, caps);
                    driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
                    break;
                case "iOS":
                    caps.setCapability(MobileCapabilityType.AUTOMATION_NAME, prop.getProperty("iOSAutomationName"));
                    String iOSappURL = getClass().getResource(prop.getProperty("iOSAppLocation")).getFile();
                    caps.setCapability("udid", udid);
                    caps.setCapability(MobileCapabilityType.APP, iOSappURL);
                    caps.setCapability("bundleId", prop.getProperty("iOSBundleId"));
                    caps.setCapability("autoAcceptAlerts", "true");
                    caps.setCapability("wdaLocalPort",wdaLocalPort);
                    caps.setCapability("derivedDataPath",prop.getProperty("iOSDerivedDataPath"));
                    driver = new IOSDriver(url, caps);
                    driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
                    break;
                default:
                    throw new Exception("Invalid Platform" + platformName);
            }
            setDriver(driver);
            setAllureEnvironment(prop.getProperty("iOSAppLocation"), prop.getProperty("androidAppLocation"));

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                inputStream.close();

            }
            if (stringsis != null) {
                stringsis.close();
            }
        }
    }

    public void waitForVisibility(MobileElement e) {
        WebDriverWait wait = new WebDriverWait(getDriver(), Testutils.WAIT);
        wait.until(ExpectedConditions.visibilityOf(e));
    }

    public void Click(MobileElement e) {
        waitForVisibility(e);
        e.click();
    }

    public void clear(MobileElement e) {
        waitForVisibility(e);
        e.clear();
    }

    public void sendKeys(MobileElement e, String txt) {
        clear(e);
        e.sendKeys(txt);
    }

    public String getAttribute(MobileElement e, String attribute) {
        waitForVisibility(e);
        return e.getAttribute(attribute);
    }

    public String getText(MobileElement e) {
        waitForVisibility(e);
        ElementDisplayed(e);
        switch (getPlatform()) {
            case "Android":
                return getAttribute(e, "text");
            case "iOS":
                return getAttribute(e, "label");
        }
        return null;
    }

    public void ElementDisplayed(MobileElement e) {
        waitForVisibility(e);
        e.isDisplayed();
    }

    public void appReset() {
        getDriver().resetApp();
    }

    @AfterTest(alwaysRun = true)
    public void QuitDriverSession() {
        if (getDriver() != null){
            System.out.println("quit==="+getDriver().getSessionId());
            getDriver().quit();
            driver.remove();
        }
    }

package com.qa.tests;

import com.qa.BaseTest;
import com.qa.pages.LoginPage;
import com.qa.pages.SetReminderDayPage;
import com.qa.pages.WelcomeHomePage;
import io.qameta.allure.*;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.testng.Assert;
import org.testng.annotations.*;

import java.io.InputStream;
import java.lang.reflect.Method;

public class LoginTest extends BaseTest {
    JSONObject loginUsers;
    WelcomeHomePage welcomeHomePage;
    LoginPage loginPage;
    SetReminderDayPage setReminderDayPage;

    @BeforeClass
    public void LoadTestData() throws Exception {
        InputStream data = null;
        try {
            String dataFileName = "data/Users.json";
            data = getClass().getClassLoader().getResourceAsStream(dataFileName);
            JSONTokener tokener = new JSONTokener(data);
            loginUsers = new JSONObject(tokener);
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (data != null) {
                data.close();
            }
        }
    }

    @BeforeMethod
    public void initializePageObject(Method m) {
        welcomeHomePage = new WelcomeHomePage();
    }


    @Feature("Login")
    @Story("User enters Corrent Credentials")
    @Severity(SeverityLevel.BLOCKER)
    @Test(description = "Check Login with Valid Credentials")
    public void validCredentials() {
        appReset();
        loginPage = welcomeHomePage.ClickloginLabel();
        switch (getPlatform()) {
            case "Android":
                loginPage.enterUserName(loginUsers.getJSONObject("androidValidUser").getString("username"));
                loginPage.enterPassword(loginUsers.getJSONObject("androidValidUser").getString("password"));
                break;
            case "iOS":
                loginPage.enterUserName(loginUsers.getJSONObject("iOSValidUser").getString("username"));
                loginPage.enterPassword(loginUsers.getJSONObject("iOSValidUser").getString("password"));
                break;
        }
        setReminderDayPage = loginPage.clickLoginButton();
        String actualTitle = setReminderDayPage.getTitle();
        String expectedTitle = getStrings().get("reminder_day_Screen_title");
        Assert.assertEquals(actualTitle, expectedTitle);
    }

    @Feature("Login")
    @Story("User See all UI elemets visible")
    @Test(description = "Check all Elements Displayed on Screen")
    public void AllElementsDisplayed() {
        loginPage = welcomeHomePage.ClickloginLabel();
        loginPage.AllElementsDisplayed();
    }

    @Feature("Login")
    @Story("User Enter Wrong Credentials")
    @Test(description = "Check Login with Invalid Credentials")
    public void invalidCredentials() {
        appReset();
        loginPage = welcomeHomePage.ClickloginLabel();
        loginPage.enterUserName(loginUsers.getJSONObject("invalidUser").getString("username"));
        loginPage.enterPassword(loginUsers.getJSONObject("invalidUser").getString("password"));
        loginPage.clickLoginButton();
        String actualErrText = loginPage.getErrText();
        String expectedErrText = getStrings().get("login_fail_alert_text");
        Assert.assertEquals(actualErrText, expectedErrText);
        String actualErrTitle = loginPage.getErrTitle();
        String expectedErrTitle = getStrings().get("login_fail_alert_title");
        Assert.assertEquals(actualErrTitle, expectedErrTitle);

    }

    @Feature("Login")
    @Story("User Enter Only password")
    @Test(description = "Check Login with valid Password Only")
    public void validPasswordOnly() {
        appReset();
        loginPage = welcomeHomePage.ClickloginLabel();
        loginPage.enterPassword(loginUsers.getJSONObject("androidValidUser").getString("password"));
        loginPage.clickLoginButton();
        String actualEmailerrotext = loginPage.getWrongEmailErrorText();
        String expectedEmailerrorText = getStrings().get("login_fail_pass_only");
        Assert.assertEquals(actualEmailerrotext, expectedEmailerrorText);
    }
}

Please any help on this will be greatly appreciated, I’m stuck on this for some time now :frowning:

Regards,
Abdul

you should have 2 different appium server while running.

url = new URL(prop.getProperty("AppiumURL"))

this should be different

Thanks @Aleksei for your answer

but its not possible to have them running on a single Appium server ? I’m already using Threadlocal & different port numbers for each test?

no. each device should have it own server. i mean number of parallel thread = number of appium servers.

https://appium.io/docs/en/advanced-concepts/parallel-tests/

Note, that it is not possible to have more than one session running on the same device.

@Aleksei Thank you soo much, I appreciate it. I will try this with 2 appium servers.

One last question: Can I also safely remove port numbers for each test as well i.e systemPort & wdaLocalPort ? since only one test will be running at a time on a single server ?

yes. better move them to some device config file. you still need configure different e.g. WDA ports…

1 Like

@Aleksei I’m still having kind of similar issue, i.e now I started 2 different appium server on 2 different ports & giving different URL for android & ios drivers

for android
url = new URL(prop.getProperty(“AppiumServer”)+ “4723/wd/hub”);

for iOS
url = new URL(prop.getProperty(“AppiumServer”)+ “4724/wd/hub”);

now even though they are running on seperate appium servers, when i.e one test finishes earlier it still closes the other one while other one is trying to find the element but app crashed or closed but now I started to get this error, this is happening only for iOS since android finishes earlier abit.

Expected condition failed: waiting for visibility of Located by By.chained({By.xpath: (//XCUIElementTypeLink[@name="xxx"])}) (tried for 12 second(s) with 500 milliseconds interval)

Some ios server logs with errors:

[debug] [simctl] Error running 'terminate': An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=3):
Application termination failed.
FBSSystemService reported failure without an error, possibly because the app is not currently running.
[XCUITest] Reset: failed to terminate Simulator application with id "xxxx"

Thanks for your help :pray:

This is currently sequence of execution happening for with testNG thread count 2:

-----------Android—driver STARTED with SessionID: 3709cdd0-e24f-4499-bf17-29461eb967df-----------
-----------iOS—driver STARTED with SessionID: 39ad63ed-2384-4d51-8442-b1feaf70e522-----------

first android test finishes earlier
-----------Android—driver QUITED with SessionID: 3709cdd0-e24f-4499-bf17-29461eb967df-----------
-----------iOS—driver STARTED with SessionID: 7bfe770a-8caa-4d40-949e-07c25e042bc0-----------
-----------iOS—driver QUITED with SessionID: 39ad63ed-2384-4d51-8442-b1feaf70e522-----------
-----------Android—driver STARTED with SessionID: e572838a-b3e2-46e9-b388-4065f391af3d-----------
-----------iOS—driver QUITED with SessionID: 7bfe770a-8caa-4d40-949e-07c25e042bc0-----------
-----------Android—driver QUITED with SessionID: e572838a-b3e2-46e9-b388-4065f391af3d-----------

I observed that because the squence of tests in testng it tries to start another iOS test while the previous one was still running, is there a way how we can control this ? i.e let previous ios or android test finish before starting a new one ?

Put your code into github and send a link

@Aleksei Thank you for all your help, I solved the issue. so it was happening because of sequential order of running tests from testNG, basically spinning up new driver instance on same device while previous one was still executing tests. What I wanted to achieve was, android & ios simulator to run tests in parallel but tests should run on each device in sequence. Now also they are running on seperate appium server thanks to you :pray:

I solved it through TestNG by adding test classes on the class level & initializaing & quiting driver in beforeClass & AfterClass annotations. if this helps anyone adding testNG

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="xxxxxx" parallel="tests" thread-count="2">
    <listeners>
        <listener class-name="com.qa.listeners.testListener"/>
    </listeners>
    <test name="iOS Tests">
            <parameter name="platformName" value="iOS"/>
            <parameter name="deviceName" value="iphone12"/>
            <parameter name="emulator" value="true"/>
            <parameter name="platformVersion" value="15.5"></parameter>
            <parameter name="udid" value="55896861-DB90-4F13-955A-F65D20F409B3"></parameter>
            <parameter name="wdaLocalPort" value="10000"></parameter>
            <classes>
                <class name="com.qa.tests.E2ERegistrationTest"/>
                <class name="com.qa.tests.LoginTest"/>
                <class name="com.qa.tests.LogoutTest"></class>
                <class name="com.qa.tests.TodayScreenCardsTest"/>
            </classes>
    </test>
    <test name="Android Tests">
        <parameter name="platformName" value="Android"/>
        <parameter name="deviceName" value="Pixel_3"/>
        <parameter name="emulator" value="true"/>
        <parameter name="platformVersion" value="13.0"></parameter>
        <parameter name="udid" value="emulator-5554"></parameter>
        <parameter name="systemPort" value="11000"></parameter>
        <parameter name="mjpegServerPort" value="12000"></parameter>
        <classes>
            <class name="com.qa.tests.E2ERegistrationTest"/>
            <class name="com.qa.tests.LoginTest"/>
            <class name="com.qa.tests.LogoutTest"></class>
            <class name="com.qa.tests.TodayScreenCardsTest"/>
        </classes>
    </test>
</suite>