Test automation with Appium

Stack:

Project

External

Client

Server

Highover

Appium Driver

instruction 

 E.g.

closeApp / launchApp

+

Capabilities

 

{
  "browserName": "Android",
  "version": "5.0.1",
  "maxInstances": 1,
  "platform": "ANDROID",
  "deviceName": "Android5",
  "avd": "Android5"
}

Highover multiple devices parallel

Project?

Client

Dependency (.jar)

management

...
<modelVersion>4.0.0</modelVersion>
<groupId>nl.energiedirect</groupId>
<artifactId>EdAppTestAutomation</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>EdAppTestAutomationProject</name>
<dependencies>
    <dependency>
        <groupId>info.cukes</groupId>
        <artifactId>cucumber-core</artifactId>
        <version>1.2.5</version>
    </dependency>
    <dependency>
        <groupId>io.appium</groupId>
        <artifactId>java-client</artifactId>
        <version>4.1.2</version>
    </dependency>
    ...

pom.xml

  • Packages
  • Object-oriented
  • Strongly typed
  • Static vs Instance
package nl.energiedirect.people;

public class Person {
    private String name;

    public constructor(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String setName(String name) {
        this.name = name;
    }   
}
package nl.energiedirect;

import nl.energiedirect.people.Person;

public class Test {
    // This method makes this file executable
    public static void main(String args[]) {
        Person charles = new Person("Charles");
        System.out.println(charles.getName());
        // Charles
    } 
}

Features

Exceptions

...
public URL createUrl() {
    return new URL("http://foo");
}
...

Unhandled exception

...
public URL createUrl() throws MalformedURLException {
    return new URL("http://foo");
}
...

...
try {
    createUrl();
} catch (MalformedURLException m) {
    m.printStackTrace();
}
...
...
public static Optional<URL> createUrl() {
    try {
        return Optional.of(new URL("http://foo"));
    } catch (MalformedURLException e) {
        return Optional.empty();
    }
}
...
...
Optional<URL> urlOptional = createUrl();
if (urlOptional.isPresent()) {
    URL url = urlOptional.get();
} else {
    // Failed to create URL
}
...
  • Throwing
  • Optionals

Java 8

Streams primitive types

String[] listOfWords = new String[]{"Hello", "World", "foo", "bar"};
// Set up stream (nothing gets executed yet)
Arrays.stream(listOfWords)
        // Chain an operator
        .filter(word -> word.equals("Hello") || word.equals("World"))
        // Apply a terminal action (which closes the stream, streams are not reusable).
        .forEach(System.out::println);// <- method reference
        // Same as: .forEach(word -> System.out.println(word));

// Save the result as primitive type
String[] listOfHelloWorldWords = Arrays.stream(listOfWords)
        // Chain an operator
        .filter(word -> word.equals("Hello") || word.equals("World"))
        // Apply a terminal action (which closes the stream, streams are not reusable).
        .toArray(String[]::new); 

Java 8

Streams

List<String> listOfWords = new ArrayList<>();
listOfWords.add("Hello");
listOfWords.add("World");
listOfWords.add("foo");
listOfWords.add("bar");
// Save the result as a more complex type
List<String> listOfHelloWorldWords = listOfWords
        // Non primitive types usually have a stream method
        .stream()
        // Chain an operator
        .filter(word -> word.equals("Hello") || word.equals("World"))
        // Apply a terminal action (which closes the stream, streams are not reusable).
        .collect(Collectors.toList());

Advanced Dependency injection

public interface MakeSound {}

@Component
public abstract class Animal implements MakeSound {}

public class Cat extends Animal {}

public class Dog extends Animal {}

@Component
public class Person implements MakeSound {}

@Component
public class Example {
    @Autowired
    private Dog dog;

    @Autowired
    List<Animal> animals; // Cat, Dog

    @Autowired
    List<MakeSound> thingsThatMakeSound; // Cat, Dog, Person
}

Other info

  • Spring looks for applicationContext.xml on the classpath (src/test/resources are copied to the classpath)
  • Because of Cucumber cucumber.xml is loaded instead of the applicationContext. Cucumber.xml imports applicationContext.xml. (So config is split over 2 files).
  • Spring is a massive framework. For this project we only use dependency injection. Lots of docs online.

Client

Page object pattern

@Component
public class AbstractPageObject {
    // Common logic
    // For example: 
    public boolean isDisplayed() {
        return this.getVisibilityElement().isDisplayed();
    }
    // Force implementing class to decide what element should be checked.
    public abstract MobileElement getVisibilityElement();
}

ExpectationsPage

@Component
public class ExpectationsPage extends AbstractPageObject {

    private final String toLoginButtonAutomationText = "continueToLogin";
    @AndroidFindBy(accessibility = toLoginButtonAutomationText)
    @iOSFindBy(accessibility = toLoginButtonAutomationText)
    public MobileElement toLoginButton;

    @Autowired
    private LoginPage loginPage;


    public LoginPage tapToLoginButton() {
        toLoginButton.tap(1, 1);
        return loginPage;
    }

    @Override
    public MobileElement getVisibilityElement() {
        return toLoginButton;
    }
}

Page rules

  • Representation of automation text elements.
  • Contains methods that act upon its elements.
  • If a button navigates to another page, then we return that page.
  • No state!

Gherkin BDD

.feature

#src/test/resources/features/Expectations.feature
Feature: Expectations

  Scenario: User continues to the next page

    Given I'm at the expectations screen
    When I tap the login screen button
    Then I see the login screen

Cucumber glue

step definitions

package nl.energiedirect.tests.step_definitions;

@Scope("cucumber-glue")
public class ExpectationsSteps implements En {
    private final Logger log = LogManager.getLogger(getClass());

    @Autowired
    private ExpectationsPage expectationsPage;

    public ExpectationsSteps() {
        When("^I tap the login screen button$", () -> {
            log.info("When I tap the login screen button");
            expectationsPage.tapToLoginButton();
        });
    }
}

Cucumber Runner

.java

@CucumberOptions(
        plugin = "json:target/cucumber.json",
        features = "src/test/resources/features/Login.feature",
        glue = "nl.energiedirect",
        tags = {"~@Ignore"}
)
public class RunCukesTest {
}

Cucumber Runner

 

Gherkin feature

(resources/features)

Cucumber glue

 

Page objects

Hooks

@Before

@After

(nl.energiedirect.tests.step_definitions)

(nl.energiedirect.tests.page_objects)

(nl.energiedirect.tests.RunCukesTest)

Example hook

package nl.energiedirect.tests.step_definitions;

...

@Scope("cucumber-glue")
public class Hooks {

    public static Scenario scenario;
    private final Logger log = LogManager.getLogger(getClass());

    @Before
    public void test(Scenario scenario) {
        Hooks.scenario = scenario;
        log.info("--------------------");
        log.info("Scenario: " + scenario.getName());
    }
}

Parallel TestRunner

(resources/TestNG.xml)

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite" parallel="tests" thread-count="2" verbose="1">
    <listeners>
        <listener class-name="nl.energiedirect.utils.listeners.ErrorListener"/>
    </listeners>
    <test name="Android5">
        <parameter name="nodeConfig" value="appium-android5.json"/>
        <classes>
            <class name="nl.energiedirect.tests.RunCukesTest"/>
        </classes>
    </test>
    <test name="Android6">
        <parameter name="nodeConfig" value="appium-android6.json"/>
        <classes>
            <class name="nl.energiedirect.tests.RunCukesTest"/>
        </classes>
    </test>
</suite>

RunCukesTest.java

A single test

nodeConfig

appium-node.config.json

fetch based on nodeConfig param

Create Appium instance based on config and connect to Selenium Grid

@CucumberOptions(
        plugin = "json:target/cucumber.json",
        features = "src/test/resources/features/Expectations.feature",
        glue = "nl.energiedirect",
        tags = {"~@Ignore"}
)
public class RunCukesTest extends AbstractTestNGCucumberTests {
    private final Logger log = LogManager.getLogger(getClass());

    @BeforeTest
    @Parameters({"nodeConfig"})
    public void initDriver(String nodeConfigFile) throws MalformedURLException {
        String nodeConfigPath = getNodeConfigPath(nodeConfigFile);
        NodeConfigDTO config = NodeConfigLoader.load(nodeConfigPath);

        AppiumDriver driver = DriverFactory.createInstance(config);
        LocalDriverManager.setDriver(driver);

        CapabilitiesDTO current = config.getCapabilities()[0];
        LocalDriverManager.setCapabilities(current);
        log.info("Instantiated Appium driver: " + current);
    }

    @AfterTest
    public void quitDriver() {
        LocalDriverManager.getDriver().quit();
        log.info("Quit driver " + LocalDriverManager.getCapabilities());
    }
}

ThreadLocal

Helps separating parallel tests

public class Driver {
    public static AppiumDriver driver;

    // vs    

    public static ThreadLocal<AppiumDriver> localDriver = new ThreadLocal<>();

    public static AppiumDriver getDriver() {
        return mobileDriver.get();
    }
}

// Driver.driver = The same for each parallel test.
// Driver.getDiver() = Different for each parallel test.

In parallel

Lifecycle

@BeforeSuite // TestNG

@BeforeTest // TestNG

@Before // Cucumber

Scenario // Cucumber

@After // Cucumber

@AfterTest

@AfterSuite

Once per device

Once per defined scenario

Once

Generic StepDefinition

PageIdentifier

get(gherkinName)

PageObject page

List<AbstractPage>

getGherkinAliases()

String[] aliases

Generic StepDefinition

Navigation

goToPage(page)

boolean navigationHandled

List<Navigator>

goTo(page)

boolean navigationHandled

AutomationTests

By rachnerd

AutomationTests

  • 394