PageObject Initialization Best Practices

Hello,

In my team we’re doing cross platform UI testing using Appium and the Appium Java-Client.
The current structure of our project is something like:

mobile
    pages
        SignInPage
    steps
        SignInSteps

The steps are “glued” together using Cucuember.
SignInPage looks something like this:

public class SignInPage {

    public SignInPage(AppiumDriver driver) {
        PageFactory.initElements(new AppiumFieldDecorator(driver, 15, TimeUnit.SECONDS), this);
    }

    // region Identifiers
    final String IOS_USERNAME_FIELD = "SignInUsernameField";
    final String ANDROID_USERNAME_FIELD = "new UiSelector().resourceIdMatches(\".*id/username.*\")";
    final String IOS_PASSWORD_FIELD = "SignInPasswordField";
    final String ANDROID_PASSWORD_FIELD = "new UiSelector().resourceIdMatches(\".*id/password_editText.*\")";
    final String IOS_SIGN_IN_BUTTON = "SignInButton";
    final String ANDROID_SIGN_IN_BUTTON = "new UiSelector().resourceIdMatches(\".*id/signInButton.*\")";
    // endregion

    @iOSFindBy(accessibility = IOS_USERNAME_FIELD)
    @AndroidFindBy(uiAutomator = ANDROID_USERNAME_FIELD)
    private MobileElement usernameField;

    @iOSFindBy(accessibility = IOS_PASSWORD_FIELD)
    @AndroidFindBy(uiAutomator = ANDROID_PASSWORD_FIELD)
    private MobileElement passwordField;

    @iOSFindBy(accessibility = IOS_SIGN_IN_BUTTON)
    @AndroidFindBy(uiAutomator = ANDROID_SIGN_IN_BUTTON)
    private MobileElement signInButton;

    public MobileElement getUsernameField() {
        return usernameField;
    }

    public MobileElement getPasswordField() {
        return passwordField;
    }

    public MobileElement getSignInButton() {
        return signInButton;
    }

    public void tapUsernameField() {
        getUsernameField().click();
    }

    public void tapSignInButton() {
        getSignInButton().click();
    }

    public void clearUsernameEditText() {
        getUsernameField().clear();
    }
}

We’re not sure in terms of performance and elements lookup where is it best to create an instance of the SignInPage. Currently we have a @Before method in our SignInSteps that is executed before each Gherkin scenario starts (which is not ideal) but it helps us having a SignInPage property in the SignInSteps class that is reused by all the steps.

public class SignInSteps {

    private SignInPage signInPage;
    AppiumDriver driver;

    @Before()
    public void setUp() throws MalformedURLException {
        driver = TestBase.getInstance().getDriver();
        signInPage = new SignInPage(driver);
    }

    @Given("I fill in the username and password")
    public void fill_username_and_password() throws Throwable {
        signInPage.tapUsernameField();
        signInPage.clearUsernameEditText();
        fillEditText(signInPage.getUsernameField(), PropertiesManager.getInstance().getValueForKey(Constants.SIGN_IN_USERNAME));
        fillEditText(signInPage.getPasswordField(), PropertiesManager.getInstance().getValueForKey(Constants.SIGN_IN_PASSWORD));
    }
  // Other sign in steps below
}

However I feel that a cleaner approach would be to create the SignInPage as a local variable inside each step method in SignInSteps. Is there any performance impact in creating the page(s) you need in each step?

Also, it’s not clear to me, with our current approach (the @Before approach) why exactly does it work even when you create a page for some steps that will be executed later on (so the screen is not even visible at this point).

So maybe the larger question would be how are the elements looked up? Is it when calling PageFactory.initElements(new AppiumFieldDecorator(driver, 15, TimeUnit.SECONDS), this); or when actually accessing the annotated properties (which would be some kind of lazy initialization approach that from my knowledge Java doesn’t have, unless my understanding of Java annotations is wrong).

Sorry for the long post, but these are some things that I want to understand thoroughly. So any help is highly appreciated.

Thank you!

our iOS and Android app are quite similar. BUT due to core differences in OS I use following approach:

Some AndroidPage
                ---- Android Base Page
				                      ---- Base Page
                ---- iOS Base Page
Some iOSPage

Some AndroidTest
                ---- Android Base Test
				                      ---- Base Test
                ---- iOS Base Test
Some iOSTest

Base Test/Page contain same core elements
Android/iOS Base Test/Page contain OS specific

Example:
dologin (username, password) in same Android/iOS test working slightly in different ways.

I found current solution slightly easy to maintain cause some Pages are really large and when include OS specific “IFs” inside single page - this makes it more harder to read. Although test itself code may look absolutely the same and duplicating sometimes (iOS vs Android).

1 Like

Thank you for your answer but my question is more around best practices for instantiating the page and how the element lookup works behind the hood from a performance standpoint.

some speed concern mentioned here https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/migrating-to-xcuitest.md

Still, that doesn’t provide a full understanding of when / where should a page be instantiated.

i am initiating whenever page first time needed and only one time in test. also i add “driver” as parameter to make it work with multiple connected phones when i need in test send something from one phone to another one.

@Aleksei if you can share some java code for this, that would be really grateful.

@crujzo

public abstract class Page {

    protected WebDriver driver;
    private int defaultLook = 20; //default look for elements
    private int fastLook = 7; // wait for 7 sec

    public Page(WebDriver driver) {
        this.driver = driver;
        setDefaultTiming();
    }

    public void setDefaultTiming() {
        PageFactory.initElements(new AppiumFieldDecorator(driver, defaultLook, TimeUnit.SECONDS), this);
    }

    public void setFastLookTiming() {
        PageFactory.initElements(new AppiumFieldDecorator(driver, fastLook, TimeUnit.SECONDS), this);
    }
}

public class SomePage extends Page {

    // top
    @iOSFindBy(id = "headerTitle")
    private List<IOSElement> headerTitle;

    // main container
    @iOSFindBy(id = "someViewControllerScene_someStory")
    private List<IOSElement> mainContainer;

    // button
    @iOSFindBy(id = "myButton")
    private List<IOSElement> myButton;

    public SomePage(WebDriver driver) {
        super(driver);
    }

    public boolean isSomePageLoaded() {
        System.out.println("  verify 'Some Page' Screen loaded");
        boolean bool;
        setFastLookTiming();
        bool = !mainContainer.isEmpty();
        setDefaultTiming();
        return bool;
    }

    public boolean tapMyButton() {
        System.out.println("  tap 'My' button");
        try {
            return tapElement(myButton.get(0));
        } catch (Exception e) {
            return false;
        }
    }
}


public class BaseTest {
    protected static AppiumDriver driver = null;
    protected static AppiumDriver driver_2 = null;
    protected static WebDriver webDriver = null;
    private static DesiredCapabilities capabilities = null;

    public String user1_Email = "mail"
    public String user1_Pass = "pass"


    @BeforeTest(alwaysRun=true)
    @Parameters ({"myParameter", ...})
    public void beforeTest(@Optional String myParameter, ITestContext iTestContext) throws Exception {
        System.out.println("BeforeTest: ");
        // some code what needed
    }


    @BeforeMethod(alwaysRun=true)
    @Parameters ({"myAnotherParameter", ...})
    public void beforeMethod(@Optional String myAnotherParameter, Object[] testArgs, Method method, ITestContext iTestContext) throws Exception {
        // another serious code
    }

    @AfterMethod(alwaysRun = true)
    public void afterMethod(ITestResult iTestResult, ITestContext iTestContext) throws Exception {
        System.out.println("AfterMethod: ");
        System.out.println("Test " + iTestResult.getName() + ", status " + getResultAsText(iTestResult.getStatus()));
        // some shit after test e.g. take screenshot if it fails
    }

    @AfterClass(alwaysRun=true)
    @Parameters ()
    public void afterClass(ITestContext iTestContext) throws Exception {
        System.out.println("AfterClass: ");

        System.out.println("  ---------------------------");
        System.out.println("  partially completed results");
        System.out.println("   passed  tests: "+iTestContext.getPassedTests().size());
        System.out.println("   skipped tests: "+iTestContext.getSkippedTests().size());
        System.out.println("   failed  tests: "+iTestContext.getFailedTests().size());
    }

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

    @AfterSuite(alwaysRun=true)
    @Parameters()
    public void tearDown(ITestContext iTestContext) throws Exception{
        System.out.println("AfterSuite: ");
        // any finals
    }

}

public class BaseTest_iOS extends BaseTest {
    // e.g. accept iOS Alert
    public boolean acceptAlert(int waitTimeInSec) {
        System.out.println("   wait to dismiss dialog");
        WebDriverWait wait = new WebDriverWait(driver, waitTimeInSec);
        try {
            wait.until(ExpectedConditions.alertIsPresent());
            driver.switchTo().alert().accept();
            return true;
        } catch (Exception e) {
            System.err.println("   no alert visible after "+waitTimeInSec+" sec.");
            return false;
        }
    }
}

// now some real test
public class mySuperTest extends BaseTest_iOS {

    private MainPage mainPage;
    private SwtichPage switchPage;

    @Test
    public void do_mySuperTest() {

        //do test
        System.out.println("---------- do test ----------");
        doLogin(user1_Email,user1_Pass);
        mainPage = PageFactory.initElements(driver, MainPage.class);
        assertTrue("'Switch Account' icon NOT loaded", mainPage.tapSwitch());

        switchPage = PageFactory.initElements(driver, SwitchPage.class);
        assertTrue("'Switch' screen NOT loaded", switchPage.isSwitchPageLoaded());
        assertTrue("Tap by name SomeUserName FAILED", switchPage.tapByName("SomeUserName"));

    }
}
2 Likes

By looking at your mySuperTest class it looks that you instantiate the page in each test.
Also, your infrastructure is slightly different than mine because I use Cucuember-JVM which doesn’t allow sharing state (therefore pages) between steps, and that’s why it would make sense to create a page for each step.

Still, regardless of the infrastructure and other patterns used I think the question that I need an answer to is: Are the elements looked up when we call PageFactory.initElements ? If so, is it a performance overhead to instantiate a page every time you need it?

Is there maybe someone part of the Appium team that can share some light on this?

how you want use page without init? at least one time if we need to check/tap something on this screen we need init it. one time per test. you only init pages you needed in test and when first time you need ( or you may init all needed in begining).

i checked how long it is taking with mine mac:

Long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
    mainPage = PageFactory.initElements(driver, MainPage.class);
}
System.out.println("   - we slept: " + (System.currentTimeMillis() - startTime));

Result:

  • Page with 17 elements = 4177 = 0.4 sec per one init
  • Page with 4 elements = 862 = 0.08 sec per one init

My understanding (and I’m not sure where I’ve read this, I’ll have to look it up again) was that the element lookup isn’t done in PageFactory.initElements but it is actually done when using the annotated elements (can’t tell how this lazy initialization was implemented in Java using annotations :-?).

But I might be wrong.

elements look up not done when init :slight_smile: otherwise i will wait this init forever for 10k inits.

1 Like

Then probably the look up is done when the element is first used.
However, based on my basic Java experience, I wasn’t aware that you can create a lazy property using annotation.

Can someone share some more light on how this lookup works behind the scenes?

1 Like

I did some more research (debugging) and I’ve found the answer:

When you call PageFactory.initElements(new AppiumFieldDecorator(driver, 15, TimeUnit.SECONDS), this); the annotated properties from the page are set (decorated) via reflection (see AppiumFieldDecorator) with a proxy (ElementInterceptor) that wraps a MobileElement. Each time you call a method on the annotated property you actually call the proxy that looks up the element and forwards the method call. There is no cache in between (as opposed to WidgetInterceptor which I didn’t figured out yet where it is used).

So in my case, creating the page once, or in each step doesn’t really make a difference because the element lookup is performed each time you interact with it (which I guess it’s good, but it might have a performance impact also).

I’ve also attached a few screenshots below:

Stacktrace when you call PageFactory.initElements(new AppiumFieldDecorator(driver, 15, TimeUnit.SECONDS), this);

Stacktrace when you call click on an element

Hope this helps others as well.

1 Like

Hello @cosminstirbu
Screenshots are broken. could you please share again

Hi @learnappium1

Can’t attach the screenshots as I don’t have them any longer and they are not that relevant anyway.

The conclusion is that whenever you decorate your elements, Appium actually sets a proxy on each element via reflection.

This means that each time you do a myLabel.getText() - Appium Client makes a request to the Appium Server - so the actual initialization / decoration of the page doesn’t have any performance impact. Accessing properties on the elements, is when all the action takes place and where you can be impacted.

Hopefully all of this makes sense.

Regards,
Cosmin

1 Like

Thank you. This is the exact question that was bugging me :slight_smile: