Framework Structure

Introduction

It is in this part of the series that  you will start actually writing code.  If you are somewhat new to coding in Java, I recommend writing out all the code snippets from scratch rather than copying and pasting them, for some of the following reasons;

  • As you start writing code, you will start picking up on patterns in the code, which will help you pick up things quicker
  • As you write out the code and try to reference certain libraries, IntelliJ will auto-suggest references to import for you, which will also help you learn the code that you are writing 😀

Starting Appium Server Programatically

Because we installed Appium Server globally to our system, it has been added to our PATH and thus, we are able to start and stop the Appium Server programatically in Java using ‘AppiumDriverLocalService’. Let’s set this up now 😀

  1. With the project open in IntelliJ, right-click on the ‘utils.appium’ package and select ‘New’ -> ‘Java Class’
  2. Name the class ‘AppiumServer’ and click OK
  3. Add the following ‘private static AppiumDriverLocalService’ variable in the class and also add two static methods to start and stop the Appium Server respectively, the whole thing should look like below…
    package utils.appium;
    
    import io.appium.java_client.service.local.AppiumDriverLocalService;
    
    public class AppiumServer {
    
        private static AppiumDriverLocalService appiumServer = AppiumDriverLocalService.buildDefaultService();
    
        public static void start() {
            appiumServer.start();
        }
    
        public static void stop() {
            appiumServer.stop();
        }
    }

utils.drivers package

Editing the ‘AndroidAppDriver’ Class

Appium has come a long way over the years and made it a lot easier to connect to the devices we want to connect to.  Because we will be specifying that we are using AndroidDriver, which is of course specific to the Android platform, we do not need to declare the ‘platformName’ in our Desired Capabilities. If we instead used the more general parent class of AppiumDriver that AndroidDriver inherits from, then it would need the ‘platformName’ defined in the Desired Capabilities to distinguish between iOS and Android.

All we really need to set in our DesiredCapabilities is the ‘deviceName’ (of the Android Virtual Device), the app we want to test and the ‘avd’ name (should be same as deviceName). A full list of Desired Capabilities can be found at http://appium.io/docs/en/writing-running-appium/caps/

In this blog series, we will automate the Reddit Android app.

  1. Download the Reddit APK from https://reddit-official-app.en.uptodown.com/android/download
  2. After downloading the Reddit APK file, put it in a suitable directory (e.g. the /src folder of your local project).

Next let’s setup our AndroidDriver 🙂

  1. Edit the ‘AndroidAppDriver’ class file so it looks similar to below…
    package utils.drivers;
    
    import io.appium.java_client.android.AndroidDriver;
    import org.openqa.selenium.remote.DesiredCapabilities;
    import utils.appium.AppiumServer;
    import java.io.File;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class AndroidAppDriver {
    
        private static AndroidDriver driver;
    
        public static AndroidDriver loadNexus5xOreo() throws MalformedURLException {
            AppiumServer.start();
    
            File file = new File("src");
            File fileApp = new File(file, "reddit-official-app-3-12-0.apk"); //set app filepath to /src/[name-of-apk-file]
    
            DesiredCapabilities cap = new DesiredCapabilities();
            cap.setCapability("avd", "Nexus5xOreo"); //set the AVD (Android Virtual Device) to be launched
            cap.setCapability("deviceName", "Nexus5xOreo"); //set the name of the device to be launched (should be same as AVD)
            cap.setCapability("app", fileApp.getAbsolutePath()); //set the app to install and use as the one in the filepath specified above
    
            driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), cap); //set the AndroidDriver to an Appium session with the above DesiredCapabilities
            return driver;
        }
    }

In the above, we create a loadAndroidDriver() method which returns AndroidDriver.

At the start of this method, we start up the Appium Server.

We then set the filepath of the APK file to install on the device.

Next, we set the DesiredCapabilities that we want in our Appium session when we load this Android Driver…

  • avd – name of our Android Virtual Device
  • deviceName – name of our device (should match AVD name in this example)
  • app – filepath to the APK file we want to use

We finally set our AndroidDriver to an Appium session with the specified DesiredCapabilities and then return the driver to be used.

(Note that, we could also add a capability for ‘automationName’, where we could for example, specify that we want to use Android Espresso instead of UIAutomator2.  The tests would be written exactly the same for the most part, only difference is Espresso may run a bit faster, and you can also locate elements via Android view tag.)

utils.appium package

Editing the ‘DriverController’ Class

  1. Open up the ‘DriverController’ class in the ‘utils.appium’ package and edit the class file to match below, to create an instance of DriverController which can be used to start the AndroidDriver and device (we have also used Log4J2 here to report any errors when stopping the Driver)…
    package utils.appium;
    
    import io.appium.java_client.android.AndroidDriver;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import utils.appium.AppiumServer;
    import utils.drivers.AndroidAppDriver;
    import java.net.MalformedURLException;
    
    public class DriverController {
    
        public static DriverController instance = new DriverController();
    
        AndroidDriver androidDriver;
    
        private static Logger log = LogManager.getLogger(DriverController.class);
    
        public void startNexus5xOreo() throws MalformedURLException {
            if (instance.androidDriver != null) return;
            instance.androidDriver = AndroidAppDriver.loadNexus5xOreo();
        }
    
        public void stopAppDriver() {
            if (instance.androidDriver == null) return;
    
            try {
                instance.androidDriver.quit();
            } catch (Exception e) {
                log.error(e + ":: AndroidDriver stop error");
            }
    
            AppiumServer.stop();
            instance.androidDriver = null;
            log.debug(":: AndroidDriver stopped");
        }
    }

Editing the ‘Driver’ Class

  1. Open up the ‘Driver’ class in the ‘utils.appium’ package and edit the class file to match below, to create a public AndroidDriver instance which returns an instance of DriverController when initiated, which can then be used in test methods (e.g. finding elements or implicitly waiting etc)…
    package utils.appium;
    
    import io.appium.java_client.android.AndroidDriver;
    
    public class Driver {
        
        public static AndroidDriver appDriver() {
            return DriverController.instance.androidDriver;
        }
    }

Editing the ‘Settings’ Class

  1. In our framework, we use the Settings file to store public static variables that we will want to be using in our project.  In this blog series, we will make a simple automated test of opening the Reddit app, going to the main screen and switching from the ‘Popular’ tab to the ‘Home’ tab. This test currently does not require us to store anything in our Settings class, so we can leave it empty for now, it’s just good to know what it would be used for, e.g. storing sensitive data like login credentials (or environment variable names to where the credentials are stored).

utils.hooks package

For now, we will only be adding Before and After hooks for the top-level Cucumber scenarios/feature files.

Editing the ‘CucumberHooks’ Class

  1. Open up the ‘CucumberHooks’ class in the ‘utils.hooks’ package and edit the class file to match below.  This class file will contain all our Before and After hooks for our Cucumber scenarios, which respectively get executed before or after a Cucumber feature and/or scenario. Please see https://github.com/cucumber/cucumber/wiki/Hooks for more information…
    package utils.hooks;
    
    import io.cucumber.java.After;
    import io.cucumber.java.Before;
    import utils.appium.DriverController;
    
    import java.net.MalformedURLException;
    
    public class CucumberHooks {
    
        @Before("@Nexus5xOreo")
        public void beforeNexus5xOreo() throws MalformedURLException {
            DriverController.instance.startNexus5xOreo(); //start our Android driver and device when we run a test with "Nexus5xOreo" as the tag
        }
    
        @After
        public void stopAppDriver() {
            DriverController.instance.stopAppDriver(); //stop the Driver after the scenario or feature has run
        }
    }

pages package

Editing the ‘BasePage’ Class

  1. Open up the ‘BasePage’ class in the ‘pages’ package and edit the class file to match below.  The ‘BasePage’ class is where we will later put all our test methods for our BaseScenarios, and any methods that exist / can be shared across more than one specific page object (e.g. main navigation, validate a passed pageUrl etc)…
    (Also note we threw in a Log4J Logger for the class so we can use methods like log.debug() instead of using System.out.println() to print out information) 🙂

    package pages;
    
    import io.appium.java_client.android.AndroidDriver;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import static utils.appium.Driver.appDriver;
    
    public class BasePage extends Page {
        
        public AndroidDriver driver = appDriver();
    
        private static Logger log = LogManager.getLogger(BasePage.class.getName());
    }

Editing the ‘Page’ Class

If you’ve followed my Java Web Selenium or Appium Mobile Web blog series, you will notice that in our ‘Page’ class, we created a generic method called instanceOf() that takes the same generic class and initialises a new object from PageFactory with all the correct elements for the page initialised. It looked similar to below…

package pages;

import org.openqa.selenium.support.PageFactory;

import static utils.appium.Driver.appDriver;

public class Page {

    public static <T extends BasePage> T instanceOf(Class<T> clazz) {
        return PageFactory.initElements(appDriver(), clazz);
    }
}

This works perfectly fine if we only use Selenium’s @FindBy annotations and WebElement class.  However, when we want to use annotations or element classes specific to mobile / Appium, it is required to decorate our driver instance with an AppiumFieldDecorator. If we look at Selenium’s PageFactory class, we can see that it contains the following methods…

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.openqa.selenium.support;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.FieldDecorator;

public class PageFactory {
    public PageFactory() {
    }

    public static <T> T initElements(WebDriver driver, Class<T> pageClassToProxy) {
        T page = instantiatePage(driver, pageClassToProxy);
        initElements(driver, page);
        return page;
    }

    public static void initElements(WebDriver driver, Object page) {
        initElements((ElementLocatorFactory)(new DefaultElementLocatorFactory(driver)), (Object)page);
    }

    public static void initElements(ElementLocatorFactory factory, Object page) {
        initElements((FieldDecorator)(new DefaultFieldDecorator(factory)), (Object)page);
    }

    public static void initElements(FieldDecorator decorator, Object page) {
        for(Class proxyIn = page.getClass(); proxyIn != Object.class; proxyIn = proxyIn.getSuperclass()) {
            proxyFields(decorator, page, proxyIn);
        }

    }

    private static void proxyFields(FieldDecorator decorator, Object page, Class<?> proxyIn) {
        Field[] fields = proxyIn.getDeclaredFields();
        Field[] var4 = fields;
        int var5 = fields.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Field field = var4[var6];
            Object value = decorator.decorate(page.getClass().getClassLoader(), field);
            if (value != null) {
                try {
                    field.setAccessible(true);
                    field.set(page, value);
                } catch (IllegalAccessException var10) {
                    throw new RuntimeException(var10);
                }
            }
        }
    }

    private static <T> T instantiatePage(WebDriver driver, Class<T> pageClassToProxy) {
        try {
            try {
                Constructor<T> constructor = pageClassToProxy.getConstructor(WebDriver.class);
                return constructor.newInstance(driver);
            } catch (NoSuchMethodException var3) {
                return pageClassToProxy.newInstance();
            }
        } catch (InstantiationException var4) {
            throw new RuntimeException(var4);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException(var5);
        } catch (InvocationTargetException var6) {
            throw new RuntimeException(var6);
        }
    }
}

If we examine the above PageFactory class carefully, we can see that unfortunately, there is no initElements() method which takes both a FieldDecorator and a generic class.

This is slightly problematic as we want to implement PageFactory using our instanceOf() method which takes a generic class that we pass into it as our page class, as well as be able to use the required AppiumFieldDecorator, instead of implementing PageFactory the traditional way where we initElements() in the constructor of each page class (in this case we would just pass in the page class and wouldn’t need a generic).

To solve this without resorting to the traditional implementation of PageFactory and be able to use both a generic class with AppiumFieldDecorator, we can create our own initElements() method which first instantiates the generic class as a page class and then passes that in to the AppiumFieldDecorator overloaded initElements() method. Note that we’ve had to copy the instantiatePage() method from Selenium’s PageFactory class, due to the fact they made it private 🙁

  1. Open up the ‘Page’ class in the ‘pages’ package and edit the class file to match below…
    package pages;
    
    import io.appium.java_client.pagefactory.AppiumFieldDecorator;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.support.PageFactory;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.time.Duration;
    
    import static utils.appium.Driver.appDriver;
    
    public class Page {
    
        public static <T extends BasePage> T instanceOf(Class<T> clazz) {
            return initElements(appDriver(), clazz); //return our own custom initElements() method instead of one provided by Selenium
        }
    
        private static <T> T initElements(WebDriver driver, Class<T> pageClassToProxy) {
            T page = instantiatePage(driver, pageClassToProxy);
            PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(15)), page);
            return page;
        }
    
        private static <T> T instantiatePage(WebDriver driver, Class<T> pageClassToProxy) {
            try {
                try {
                    Constructor<T> constructor = pageClassToProxy.getConstructor(WebDriver.class);
                    return constructor.newInstance(driver);
                } catch (NoSuchMethodException var3) {
                    return pageClassToProxy.newInstance();
                }
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException var4) {
                throw new RuntimeException(var4);
            }
        }
    }

Step Definitions

Editing the ‘BaseSteps’ Class

  1. Open up the ‘BaseSteps’ class in the ‘steps’ package and edit it to match as below. This is just a placeholder step definition that inherits from ‘Page’ class, to help follow POM concepts…
    package steps;
    
    import pages.Page;
    
    public class BaseSteps extends Page {
        
    }

Editing all the other Step Definitions

  1. Open up every other Step Definitions class file and change the classes so that they all inherit/extend from the ‘Page’ class as well
    package steps;
    
    import pages.Page;
    
    public class HomeSteps extends Page {
    }

TestNG

With TestNG, a TestRunner class can be made which defines everything that you want to happen to that group of tests. To work with Cucumber, you must define the directory where your Feature files are located in the project, as well as define where the glue is that connects your Feature files to your java test methods (both hooks and step definitions). The TestRunner class can also define what happens before, after and during the tests are run.

Creating the ‘TestRunner’ Class

  1. Right-click the src/test/java directory, select ‘New’ –> ‘Java Class’ and name the class “TestRunner”
  2. Add the following lines of code in to the ‘TestRunner’ class (also note the @CucumberOptions, where the location of the feature files, glue files and any formatter plugins are defined)…
    import io.cucumber.testng.AbstractTestNGCucumberTests;
    import io.cucumber.testng.CucumberOptions;
    
    
    @CucumberOptions(
            features = "src/test/resources/features",
            glue = {"utils.hooks", "steps"},
            tags = {"~@Ignore"},
            plugin = {"html:target/cucumber-reports/cucumber-pretty",
                    "json:target/cucumber-reports/CucumberTestReport.json",
                    "rerun:target/cucumber-reports/rerun.txt"
            })
    
    public class TestRunner extends AbstractTestNGCucumberTests {
    
    }

    Interestingly, you can actually make as many TestRunner classes as you see fit for your project and then define which TestRunner classes, if any, get run within a test suite.  You define all of this within a file called ‘testng.xml’.

Creating the ‘testng.xml’ File

  1. Right-click the project root in the Project Explorer, select ‘New’ –> ‘File’ and create a file called ‘testng.xml’ and press the ‘OK’ button
  2. Open up the ‘testng.xml’ file and add the following lines of XML into the file (using similar/appropriate names for your own test suite and test name)…
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
    <suite name="Product Test Suite" verbose="1" parallel="tests" thread-count="1" configfailurepolicy="continue">
        <test name="Product Acceptance Tests" annotations="JDK" preserve-order="true">
            <classes>
                <class name="TestRunner"/>
            </classes>
        </test>
    </suite>

Building the Project

Building the Maven project

  1. Open CMD-prompt / Terminal and cd (change directory) into the root of your Maven project. For example…
    cd /Users/username/DevProjects/ProductAutomationFramework
  2. Enter the following command to build your Maven project…
    mvn clean install -DskipTests

    (we currently have no tests. If we don’t put in the skipTests command, we will get a build error around one of the cucumber report plugins we are using).

Building the Gradle project

If you have setup the project with Gradle instead of Maven, you can follow the steps below to build…

  1. Open CMD-prompt / Terminal and cd (change directory) into the root of your Maven project. For example…
    cd /Users/username/DevProjects/ProductAutomationFramework
  2. Enter the following command to build your Maven or Gradle project respectively…
    mvn package

    or

    ./gradlew build

Our core framework is now setup, in the next part, we will begin adding our test scenario 😀

Digiprove sealCopyright secured by Digiprove © 2018
Liked it? Take a second to support Thomas on Patreon!

Previous Article

Next Article

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.