Capture screenshot on test failure for parallel execution with Appium and TestNG

Want to take a screenshot on test failure for parallel execution using TestNG?
https://medium.com/@eliasnogueira/capture-screenshot-on-test-failure-for-parallel-execution-with-appium-and-testng-81f869fa2def

There’s also an example to create and execute testes in parallel for iOS and Android with just one test script using Page Objects/Page Factory.

Regards!

@Elias_Nogueira minor note for easier solution :slight_smile:

@AfterMethod(alwaysRun = true)
public void afterMethod(ITestResult iTestResult, ITestContext iTestContext) throws Exception {
	if (iTestResult.getStatus() == 2) { // 2 = failed , 3 = skipped, 1 = success
		if (driver != null) {
			saveScreenShot(iTestContext, iTestResult, driver);
		}

Great @Aleksei :slight_smile:
This one works fine on parallel execution?
And this method is placed on the test or in another place?

Best!

@Elias_Nogueira

  1. it must work in any case. it is like your listener. @afterMethod called in any case. You can easy to try.
  2. And this method is placed on the test or in another place? - did not understand what you mean. With testNG we have:
@BeforeSuite: The annotated method will be run before all tests in this suite have run.
@AfterSuite: The annotated method will be run after all tests in this suite have run.
@BeforeTest: The annotated method will be run before any test method belonging to the classes inside the <test> tag is run.
@AfterTest: The annotated method will be run after all the test methods belonging to the classes inside the <test> tag have run.
@BeforeGroups: The list of groups that this configuration method will run before. This method is guaranteed to run shortly before the first test method that belongs to any of these groups is invoked.
@AfterGroups: The list of groups that this configuration method will run after. This method is guaranteed to run shortly after the last test method that belongs to any of these groups is invoked.
@BeforeClass: The annotated method will be run before the first test method in the current class is invoked.
@AfterClass: The annotated method will be run after all the test methods in the current class have been run.
@BeforeMethod: The annotated method will be run before each test method.
@AfterMethod: The annotated method will be run after each test method. 

thus AfterMethod is called right after test completed regardless how it is run (in parallel or whatever)

By “And this method is placed on the test or in another place?” I mean: you need to place @AfterMethod (or @AfterTest) in each test script… so it’s a lot of duplication.
I know TestNG very well (at least I believe).

The topic is about parallel execution. I cannot create an strategy with any base class or related because I need to create an driver instance each execution in any device. That’s my point.

Have you used your approach in a parallel execution? If yes, can you show some snippets (as as show in the github repo)?

@Elias_Nogueira you are wrong :slight_smile:. yes it works in parallel.

lets see how it is work:

// baseTest class

public class BaseTest {
    private Process appium_Process;
    protected static AppiumDriver driver = null;
    private static DesiredCapabilities capabilities = null;
    private static String fileName = "ApiDemos-debug.apk";
    private static String userDir = "/src/test/java/resources";

    @BeforeSuite(alwaysRun = true)
    public void beforeSuite(ITestContext iTestContext) throws Exception {
        System.out.println("BeforeSuite: ");
        // start appium here if needed ( or use any remote url e.g. for selenium grid)
    }

    @BeforeMethod(alwaysRun=true)
    public void beforeMethod(Object[] testArgs, Method method, ITestContext iTestContext) throws Exception {
        System.out.println("BeforeMethod: ");
        // now open driver to needed url
        capabilities = DesiredCapabilities.android();
        capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM);
        // .... other capabilites
        driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
    }

    @AfterMethod(alwaysRun=true)
    public void afterMethod(ITestResult iTestResult, ITestContext iTestContext) throws Exception {
        System.out.println("AfterMethod: ");
        // take screenshot if failed and quit driver
        try {
            driver.quit();
        } catch (Exception e) {}
    }

    @AfterTest(alwaysRun=true)
    public void afterTest(ITestContext iTestContext) throws Exception {
        System.out.println("AfterTest: ");
        // do if needed something e.g. print number of passed and failed test. useful to see how it goes with large suites
    }

    @AfterSuite(alwaysRun=true)
    @Parameters({"generateReport"})
    public void tearDown(ITestContext iTestContext, @Optional String generateReport) throws Exception{
        System.out.println("AfterSuite: ");
        System.out.println("===============================================");
        System.out.println("  passed tests:  "+iTestContext.getPassedTests().size());
        System.out.println("  skipped tests: "+iTestContext.getSkippedTests().size());
        System.out.println("  failed tests:  "+iTestContext.getFailedTests().size());
        System.out.println("===============================================\n");
        // kill appium if needed
    }
}

// now lets see some tests!

public class test_1 extends BaseTest {  // see here we extending! BaseTest with ALL it's power of testNG annotations.
    private MainPage mainPage;

    @Test
    public void foundViews_Sweep() {
        mainPage = new MainPage(driver);
        assertTrue("Main screen NOT loaded", mainPage.isMainPageLoaded());
        assertTrue("'Views' cell NOT found", mainPage.tapCellByTitle("Views"));
        assertTrue("Header title NOT Correct", mainPage.getHeaderText().equals("API Demos"));
        // Sweep does not exist. Example of failed test.
        assertTrue("'Sweep' cell NOT found", mainPage.tapCellByTitle("Sweep"));
        assertTrue("Header title NOT Correct", mainPage.getHeaderText().contains("Sweep"));
    }

    @Test
    public void foundViews_WebView() {
        mainPage = new MainPage(driver);
        assertTrue("Main screen NOT loaded", mainPage.isMainPageLoaded());
        assertTrue("'Views' cell NOT found", mainPage.tapCellByTitle("Views"));
        assertTrue("Header title NOT Correct", mainPage.getHeaderText().equals("API Demos"));
        assertTrue("'WebView' cell NOT found", mainPage.tapCellByTitle("WebView"));
        assertTrue("Header title NOT Correct", mainPage.getHeaderText().contains("WebView"));
    }
}

in @beforeMethod you can accept parameters like in your “/src/test/java/com/eliasnogueira/TipTest.java” and open driver to needed platform and url with any test whatever like.

example:

    @BeforeMethod (alwaysRun = true)
    @Parameters({"platform", "udid", "platformVersion"})
    public void beforeMethod(@Optional String platform, @Optional String udid,
                            @Optional String platformVersion,
                            ITestContext iTestContext) throws Exception {
    driver = Utils.returnDriver(platform, udid, platformVersion); // here we put your driver start

so your parameters in:

https://github.com/eliasnogueira/appium-parallel-execution/blob/master/suite.xml

will get populated in beforeMethod were you can just add your:

driver = Utils.returnDriver(platform, udid, platformVersion);

and you will not need any more to add into each your test this line cause it will be executed right before test in beforeMethod :-). i updated code.

Awesome! \o/
I gonna do some updates in my code and test with different platforms.

Thanks for the explanation! :smiley:

@Aleksei
I’ve tested the way you explained and it doesn’t works. I mean, it worked, but not in parallel.
Every time I ran the tests in two or more devices on a grid, the tests are sequential. All test are placed in one node all the time.
Probably because I have just one instance of driver object using the BaseTest instead of have multiple driver instance, one for each node in grid that run through the suite.xml.

Any thoughts?

@Elias_Nogueira try to remove “protected static” from driver. It force point to same driver. Yes - remove “static” from all variables in baseTest.

I already did it. =/
Many driver instances will be created (changing the driver on BaseTest to public), but without point the proper node.

@Elias_Nogueira if you put each test into separate class like your examples it will work. but it is not interested.

let’s see at you suite :slight_smile: xml

<suite name="Suite" parallel="tests" thread-count="3" >

when we look at http://testng.org/doc/documentation-main.html#parallel-tests we see that there are other options

try change to:

parallel="methods"
// or
parallel="instances"

Same behaviour, and it starts sequentially instead of in parallel.

@Elias_Nogueira ok you got me :slight_smile: i tested from my side and succeeded to execute in parallel.

// BaseTestParallel
package com.mobileTemplate.base;

import io.github.bonigarcia.wdm.ChromeDriverManager;
import org.openqa.selenium.WebDriver;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.annotations.*;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * Created by Aleksei on 02.03.2018.
 */
public class BaseTestParallel {
    protected static WebDriver webDriver = null;

    @BeforeSuite(alwaysRun = true)
    public void beforeSuite(ITestContext iTestContext) throws Exception {
        System.out.println("BeforeSuite: ");
    }

    @BeforeMethod(alwaysRun=true)
    public void beforeMethod(Object[] testArgs, Method method, ITestContext iTestContext) throws Exception {
        System.out.println("BeforeMethod: ");
        //start Chrome driver
        System.out.println("  start Chrome browser");
        ChromeDriverManager.getInstance().setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("-incognito");
        DesiredCapabilities capabilities = DesiredCapabilities.chrome();
        capabilities.setCapability(ChromeOptions.CAPABILITY, options);
        webDriver = new ChromeDriver(capabilities);
        webDriver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);

    }

    @AfterMethod(alwaysRun=true)
    public void afterMethod(ITestResult iTestResult, ITestContext iTestContext) throws Exception {
        System.out.println("AfterMethod: ");
    }

    @AfterTest(alwaysRun=true)
    public void afterTest(ITestContext iTestContext) throws Exception {
        System.out.println("AfterTest: ");
    }

    @AfterSuite(alwaysRun=true)
    @Parameters({"generateReport"})
    public void tearDown(ITestContext iTestContext, @Optional String generateReport) throws Exception{
        System.out.println("AfterSuite: ");
    }

    static public void sleep(int sec) {
        try{Thread.sleep(sec*1000);}catch(Exception e){}
    }
}

// tests
package com.mobileTemplate.tests.android;

import com.mobileTemplate.base.BaseTestParallel;
import org.testng.annotations.Test;

/**
 * Created by Aleksei on 02.03.2018.
 */
public class test_parallel_1 extends BaseTestParallel {

    @Test
    public void test_1() {
        System.out.println("test_1 start");
        webDriver.get("https://mail.google.com/");
        sleep(10);
        System.out.println("test_1 end");
    }

    @Test
    public void test_2() {
        System.out.println("test_2 start");
        webDriver.get("https://mail.google.com/");
        sleep(10);
        System.out.println("test_2 end");
    }

    @Test
    public void test_3() {
        System.out.println("test_3 start");
        webDriver.get("https://mail.google.com/");
        sleep(10);
        System.out.println("test_3 end");
    }
}

// xml
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="BVT_Android" parallel="methods" thread-count="3">
    <test name="BVT All" preserve-order="true">
        <classes>
            <class name="com.mobileTemplate.tests.android.test_parallel_1">
            </class>
        </classes>
    </test>
</suite>

here are output console logs:

[TestNG] Running:
  C:\Users\Admin\Documents\Tests\mobileTemplate_2017_08_25\mobileTemplate\src\test\java\resources\test_parallel.xml
BeforeSuite: 
BeforeMethod: 
  start Chrome browser
BeforeMethod: 
  start Chrome browser
BeforeMethod: 
  start Chrome browser
Starting ChromeDriver 2.36.540470 (e522d04694c7ebea4ba8821272dbef4f9b818c91) on port 21401
Only local connections are allowed.
Starting ChromeDriver 2.36.540470 (e522d04694c7ebea4ba8821272dbef4f9b818c91) on port 26586
Only local connections are allowed.
Starting ChromeDriver 2.36.540470 (e522d04694c7ebea4ba8821272dbef4f9b818c91) on port 33701
Only local connections are allowed.
märts 02, 2018 11:55:49 PM org.openqa.selenium.remote.ProtocolHandshake createSession
test_1 start
märts 02, 2018 11:55:50 PM org.openqa.selenium.remote.ProtocolHandshake createSession
test_2 start
märts 02, 2018 11:55:50 PM org.openqa.selenium.remote.ProtocolHandshake createSession
test_3 start
test_2 end
test_1 end
AfterMethod: 
AfterMethod: 
test_3 end
AfterMethod: 
AfterTest: 
AfterSuite: 

===============================================
BVT_Android
Total tests run: 3, Failures: 0, Skips: 0
===============================================

Process finished with exit code 0

as you can see (and i can see in real) all 3 tests started in parallel and opened chrome browser. after 10 sec all stopped simultaneously.

With WebDriver it’s possible because you can run the maximum number of browser instance configured in one machine. Have you tried to execute in different machines?

In mobile execution each device is a “machine”, so you need route the test to the proper node, like you can do in a web execution. The case is: this approach (your examples) does not execute in the proper nodes, even when you set it on the test suite (the udid in mobile).

@Elias_Nogueira with mobile each device has own appium session. So you can start 3 appiim sessions on one machine on different ports and with driver start point to use specific appim session. Will modify later for sure. But i used this already when i need to test mobile chat clients to send between each other data.

Another way is start selenium grid and point each appium session to it. With driver start point it to grid. Selenium grid has tons of pluses that you do not need to care about port numbers only say what device to use - and that all. So mine current code i do not need to change anything.

Will try with 3 sessions later (2 tried already). Like in way:

  1. will start 3 appium sessions
  2. each test in xml will add device and port parameter (all android let say)
  3. drivers should start using port and device

@Elias_Nogueira ok i made it easier :slight_smile: to be closer to you. using your “appium-parallel-execution” selenium config i created 3 Android appium nodes. And successfully tested in parallel using same code. As result video is here - https://www.screencast.com/t/pzjnFtOS9e (needs flash :frowning: to see ).

console output was:

[TestNG] Running:
...
BeforeSuite: 
BeforeMethod: emulator-5556
BeforeMethod: emulator-5554
BeforeMethod: emulator-5558
Capabilities {app: C:\Users\Admin\Documents\Te..., appWaitActivity: .ApiDemos, deviceName: ANDROID, platformName: ANDROID, platformVersion: 8.0.0, udid: emulator-5556}
Capabilities {app: C:\Users\Admin\Documents\Te..., appWaitActivity: .ApiDemos, deviceName: ANDROID, platformName: ANDROID, platformVersion: 8.0.0, udid: emulator-5558}
Capabilities {app: C:\Users\Admin\Documents\Te..., appWaitActivity: .ApiDemos, deviceName: ANDROID, platformName: ANDROID, platformVersion: 8.0.0, udid: emulator-5554}
märts 03, 2018 10:55:09 PM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: OSS
test_1 start
märts 03, 2018 10:55:09 PM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: OSS
märts 03, 2018 10:55:09 PM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: OSS
test_3 start
test_2 start
test_1 end
AfterMethod: 
  driver quit
test_3 end
AfterMethod: 
  driver quit
test_2 end
AfterMethod: 
  driver quit
AfterTest: 
AfterTest: 
AfterTest: 
AfterSuite: 

===============================================
BVT_Android
Total tests run: 3, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

suite.xml

<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<suite name="BVT_Android" parallel="tests" thread-count="3">
    <test name="thread_1" preserve-order="true">
        <parameter name="udid" value="emulator-5554"/>
        <classes>
            <class name="com.mobileTemplate.tests.android.test_parallel_1">
                <methods>
                    <include name="test_1"></include>
                </methods>
            </class>
        </classes>
    </test>
    <test name="thread_2" preserve-order="true">
        <parameter name="udid" value="emulator-5556"/>
        <classes>
            <class name="com.mobileTemplate.tests.android.test_parallel_1">
                <methods>
                    <include name="test_2"></include>
                </methods>
            </class>
        </classes>
    </test>
    <test name="thread_3" preserve-order="true">
        <parameter name="udid" value="emulator-5558"/>
        <classes>
            <class name="com.mobileTemplate.tests.android.test_parallel_1">
                <methods>
                    <include name="test_3"></include>
                </methods>
            </class>
        </classes>
    </test>
</suite>

I’m gonna try today…
In your code you set the methods in parallel, and created three different tests in the same class. And it is not my case.
I will create a branch in the appium-parallel-execution and send to you.

@Elias_Nogueira parallel are “parallel=“tests”” in my code not methods. This is due to inability to set different UDIDs inside one class for different methods in testNG.