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 ‘IOSAppDriver’ 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 IOSDriver, which is of course specific to the IOS 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 IOSDriver 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 iOS Simulator), the ‘platformVersion’ (e.g. the OS like 11.4),the ‘automationName’ (e.g. ‘XCUITest’ for iOS) and the app we want to test.  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 sample Artistry app provided in the Swift-30-Projects repo which can be found at https://github.com/soapyigu/Swift-30-Projects

  1. Go to https://github.com/soapyigu/Swift-30-Projects and git clone the repo to a local folder on your machine…
    • Open up Terminal
    • Install Git if not installed already…
      brew install git
    • Change directory (cd) into a folder you want to clone the ‘Swift-30-Projects’ repository into…
      cd path/To/LocalFolder
    • Git clone the repository…
      git clone https://github.com/soapyigu/Swift-30-Projects.git
  2. Open the local repository folder and then open the ‘/Project 05 – Artistry’ directory and launch the ‘Artistry.xcodeproj’ in Xcode
    • Right-click on the Artistry.app file in the Products folder in the  Project explorer pane, and select ‘Show in Finder’
    • Copy the app into the /src/ folder of your Java (IntelliJ) project

Next let’s setup our IOSDriver 🙂

  1. In the project open in IntelliJ, edit the ‘IOSAppDriver’ class file so it looks similar to below…
    package utils.drivers;
    
    import io.appium.java_client.ios.IOSDriver;
    import org.openqa.selenium.remote.DesiredCapabilities;
    import java.io.File;
    import utils.appium.AppiumServer;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class IOSAppDriver {
    
        private static IOSDriver driver;
    
        public static IOSDriver loadIphone8() throws MalformedURLException {
            AppiumServer.start();
    
            File file = new File("src");
            File fileApp = new File(file, "Artistry.app"); //set app filepath to /src/[name-of-app-file]
    
            DesiredCapabilities cap = new DesiredCapabilities();
            cap.setCapability("platformVersion", "12.0"); //set the iOS simulator version to be launched
            cap.setCapability("deviceName", "iPhone 8"); //set the name of the device to be launched (should be same as AVD)
            cap.setCapability("automationName", "XCUITest"); //set the automation engine to use as XCUITest
            cap.setCapability("app", fileApp.getAbsolutePath()); //set the app to install and use as the one in the filepath specified above
    
            driver = new IOSDriver(new URL("http://127.0.0.1:4723/wd/hub"), cap); //set the IOSDriver to an Appium session with the above DesiredCapabilities
            return driver;
        }
    }

In the above, we create a loadIOSDriver() method which returns IOSDriver.

We then start up the Appium Server.

Then, we set the filepath of the .app file to install on the device.

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

  • platformVersion – iOS version of our device simulator
  • deviceName – name of our device simulator
  • automationName – automation engine to use
  • app – filepath to the APK file we want to use

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

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 IOSDriver and device (we have also used Log4J2 here to report any errors when stopping the Driver)…
    package utils.appium;
    
    import io.appium.java_client.ios.IOSDriver;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import utils.drivers.IOSAppDriver;
    import utils.appium.AppiumServer;
    import java.net.MalformedURLException;
    
    public class DriverController {
    
        public static DriverController instance = new DriverController();
    
        IOSDriver iosDriver;
    
        private static Logger log = LogManager.getLogger(DriverController.class.getName());
    
        public void startIphone8() throws MalformedURLException {
            if (instance.iosDriver != null) return;
            instance.iosDriver = IOSAppDriver.loadIphone8();
        }
    
        public void stopAppDriver() {
            if (instance.iosDriver == null) return;
    
            try {
                instance.iosDriver.quit();
            } catch (Exception e) {
                log.error(e + ":: IOSDriver stop error");
            }
    
            AppiumServer.stop();
            instance.iosDriver = null;
            log.debug(":: IOSDriver 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 IOSDriver 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.ios.IOSDriver;
    
    public class Driver {
        
        public static IOSDriver appDriver() {
            return DriverController.instance.iosDriver;
        }
    }

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 that navigates through some screens and that is really it.  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("@Iphone8")
        public void beforeIphone8() throws MalformedURLException {
            DriverController.instance.startIphone8(); //start our iOS driver and device when we run a test with "Iphone8" 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.ios.IOSDriver;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    import static utils.appium.Driver.appDriver;
    
    public class BasePage extends Page {
        
        private IOSDriver 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 where we call initElement in each page classes constructor, 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

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;
    import org.testng.annotations.AfterTest;
    import org.testng.annotations.BeforeTest;
    import pages.BasePage;
    import utils.appium.DriverController;
    
    import java.io.IOException;
    
    import static pages.Page.instanceOf;
    
    
    @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 instead 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 project…
    ./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.