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