Appium: Running the same suite across multiple OSes
If you follow the principles in this article and utilize the open source framework [link coming soon] I refer to as a base to your Appium test suite, you will find yourself able to run the same tests across any multitude of OSes and/or device types! With the patterns I lay out below and the open source framework as a starting point, you can get a leg up on all your competitors in the mobile test automation space!
Android
We have a Search Page that the user can enter something into a search bar. The app filters the items from a list within the app and displays the filtered list as the user types in the bar.
The user can then select an item (or items) from the list and click Purchase.
The user then verifies that’s the item and price they want and then gets to a confirmation screen.iPhone
The iPhone is very similar, but while the iPhone business people liked the slick filtering the Android team came up with, they felt it was too important for the user to see prices next to the item(s) they were selecting, even before the user gets to the Buy Page. While the items are pretty static and can be stored in a list within the app, the prices change and need to be looked up via a REST call. Therefore, the iPhone implementation calls for the user performing a full search, then going to a separate item selection page, and from there the functionality is identical to Android.
This application is very simple, and will give us a good way to illustrate key concepts. There is only 1 real user requirement that we really need to test End-to-End with Appium. The test should be: The user searches for an item to purchase, and can then successfully complete the purchase of that item.
Differing behavior for the same user requirement
Both Android and iPhone have the same user requirement of being able to search for and then purchase an item from us. Despite the fact that there are slightly different implementations of our user requirement on each device, because there is only a single user requirement, we should therefore have only 1 test case for it. If the user requirement ever changes, lets say we decide to add a cart page to all of our apps, then we’d want to only update the 1 single test as opposed to different test cases for each deviceType / platform since the user requirement is the same across all of them. Differing locators Because these two apps were developed by 2 different teams, even where the behavior is identical, we have to deal with different locators. Lets examine a sample of the Buy Page from both applications:
Notice how even though they both have IDs for everything, the IDs representing the same concepts between the apps are different. This means we can’t simply use a locator of By.id(“someID”) in our Appium script. We’re going to have use some other patterns.
We can use the @AndroidFindBy and @IosFindBy annotations to differentiate between the two different sets of locators, and use them within a single PageObject class.
The SeleniumMobileBasePage that this class extends is very similar (and actually extends) the SleniumPageObject which I covered from my previous blog. The ShoppingHomePageInterface just declares the user actions that can be taken with this page object.
Explaining how the Appium Page Factory works
The OS Specific FindBy Annotations allow us to declare a different way of finding the same conceptual object between the different OSes. This solves our first issue mentioned in the Background.
We can also use the
We can assume CorporateMobileBaseTest simply launches our Android or iOS sim/emulator (or real device) and then launches our app returning us a
Explaining why the Appium Page Factory breaks design patterns
There are 2 key programming patterns that we’re in very clear violation of with this design. Single Responsibility Principle – which states that a class or method should do only 1 thing, and do it well.
We’re actually in violation of this one twice! Open/Closed Principle – which states that code should be open to extension, but closed to modification. Basically that you should be able to add things like a new
If we look back to an example from my prior blog where we were dealing with Web Page Selenium Page Objects (for a web version of our app), you could make the same argument there too. However, I think where I personally draw the line, is in that example (illustrated below), we’re basically assigning constants. Whereas in our new example (from above), we have to use Appium Annotations, which are really doing a lot of complicated logic behind the scenes for us.
While one could argue that even these constants belong in a separate class to isolate concerns, I think we can all agree that this violation becomes significantly more egregious in our mobile space where the Appium Annotations add further logic to what’s being done in our WebElement/By object’s definitions. In addition, we also have a method in our example,
This type of smell alerts us that there should clearly be two distinct searchForItem() methods like searchForItemIos() and searchForItemAndroid(). However with this Appium PageFactory pattern, there’s no easy way for the Page Object, or the Test Case to know which of these two methods to call without having an
When making updates to a locator for Android or iOS, you may inadvertently update the wrong one, or you may update a locator that has a much wider implication than you realize because your class is doing too much.
In addition, when updating a method that has different behavior between OSes, you may very easily impact an OS you didn’t intend to since all that logic is clumped together in the same method.
Lets pretend we wanted to add a WindowsPhone app, Blackberry app, or even just wanted to add our Web page app to the same test suite, since the user requirement is the same on all of those. Another common example could be if you have an iPad or Android tablet app that’s slightly different in any behavior implementation from the phone apps, but still has the same overall user requirements and you wanted to add that/those to this test suite as well. From our example above, we’d need to scan EVERY ONE of our Page Objects and see where we are making
Note: The MobileBaseInterface we’re extending is a blank Interface class, just so that we can declare that all of our Interfaces are of a “Mobile” type.
We can set any of the locator values that are common across all the OSes, in this case the searchInputField. All other locators are given abstract setters (which will force us to implement those setters within the OS specific versions of the locator classes. Lastly, we create getters so we can access these locators easily within our Page Object code.
Note: The Locators class we extend is part of the Open Source Framework [link coming soon]. This class just allows the framework to do some fancy logic to retrieve all the setters and call them to set all the appropriate values when we create the new Mobile Page Object instance.
Some of this, especially the generics (T and K) might seem a little complicated. Lets go over what this framework class does.
The
The first thing to note is that this is an
These next set of methods should look very familiar from my prior blog. It’s just the constructor and isLoaded() method:
You can notice we can call
Lastly in this common class example, we have the implementation for any user action methods that are common across ALL deviceTypes.
Our
Also note our use of our framework method here for
Note that both of these OS Specific Locator Classes each extends the common version of their locator class giving them access to all the locators we set up in it. This also has a side effect of REQUIRING us to implement all the leftover abstract methods in each of these classes for
From the class declaration, we can see that we are extending the common Implementation class
This code is nearly identical to the Android code. Only the iOS code needs to click an additional ‘Search’ button, and then land on a selection page that we want to verify correctly loads before moving on (presumably to buy the item) next. Note that while the Android class tied the
At runtime, our CorporateMobileBaseTest will launch our application on an Android device/emulator or iPhone device/simulator and bring us to our ShoppingHome Page on the appropriate device. Then we can proceed using the Interface methods in our test which will either call the method implemented in our common Impl class if the behavior is the same between both deviceTypes, or the method implemented in the OS specific full class if the behavior is OS specific.
Background:
This article is an extension of my last artilce: Flakiness of Corporate Selenium Suites and how to get rid of it. In that article we go over an open source repository [link coming soon], that allows us to create Selenium Page Objects and tests that are Stable, Maintainable, and Easy to Read / Create. This article will expand on those examples, and how to do the same in a native mobile context, as well as the differences to look out for in mobile and how to overcome mobile challenges. In particular, there are two challenges that native mobile testing has to deal with compared to Selenium tests on the web.- The locators between Android and iOS (and any other OS) are likely to be different.
- The behavior between Android and iOS (and any other OS) may be slightly different, even for the same overall user functionality.
Note: All examples in this article will be in Java, but the concepts apply to any of the Selenium/Appium language bindings.
The Example Application:
In order to explain key concepts, it will be easier to do so when referencing the following simple Application.Note: For simplicity (and because I don’t have an iOS developer licence), I am using simple webpages to illustrate an example of how native apps could work. All locators within these examples are therefore going to be web locators, but it is the concepts within this blog that you will want to apply to your Appium suite.
We have a Search Page that the user can enter something into a search bar. The app filters the items from a list within the app and displays the filtered list as the user types in the bar.
The user can then select an item (or items) from the list and click Purchase.
The user then verifies that’s the item and price they want and then gets to a confirmation screen.
The iPhone is very similar, but while the iPhone business people liked the slick filtering the Android team came up with, they felt it was too important for the user to see prices next to the item(s) they were selecting, even before the user gets to the Buy Page. While the items are pretty static and can be stored in a list within the app, the prices change and need to be looked up via a REST call. Therefore, the iPhone implementation calls for the user performing a full search, then going to a separate item selection page, and from there the functionality is identical to Android.
This application is very simple, and will give us a good way to illustrate key concepts. There is only 1 real user requirement that we really need to test End-to-End with Appium. The test should be: The user searches for an item to purchase, and can then successfully complete the purchase of that item.
Differing behavior for the same user requirement
Both Android and iPhone have the same user requirement of being able to search for and then purchase an item from us. Despite the fact that there are slightly different implementations of our user requirement on each device, because there is only a single user requirement, we should therefore have only 1 test case for it. If the user requirement ever changes, lets say we decide to add a cart page to all of our apps, then we’d want to only update the 1 single test as opposed to different test cases for each deviceType / platform since the user requirement is the same across all of them. Differing locators Because these two apps were developed by 2 different teams, even where the behavior is identical, we have to deal with different locators. Lets examine a sample of the Buy Page from both applications:
Android BuyPage snippit
<table id="item" border="2"> <tr><th>Item Name</th><th>Item Cost</th></tr> <tr><td><div id="itemName">Banana</div></td><td><div id="itemCost">$0.67</div></td></tr> </table>
iPhone BuyPage snippit
<table id="itemTable" border="2"> <tr><th>Item Name</th><th>Item Cost</th></tr> <tr><td><div id="item">Banana</div></td><td><div id="cost">$0.67</div></td></tr> </table>
Notice how even though they both have IDs for everything, the IDs representing the same concepts between the apps are different. This means we can’t simply use a locator of By.id(“someID”) in our Appium script. We’re going to have use some other patterns.
Cheating:
First I’d like to share a common industry pattern that does address both of our concerns, but in my opinion, breaks a few critical design patterns. This method is significantly simpler than using the open source framework I’ll get into later, and so if you are ok with knowingly breaking several design patterns and accepting the risk that brings on, this may be an option for you. Appium Page Factory AnnotationsWe can use the @AndroidFindBy and @IosFindBy annotations to differentiate between the two different sets of locators, and use them within a single PageObject class.
Shopping Home Page Factory Example
public class ShoppingHomePageImpl extends SeleniumMobileBasePage implements ShoppingHomePageInterface { //This one happens to be common to both iOS and Android. @FindBy(id="searchInpt") public static WebElement searchInputField; @AndroidFindBy(xpath="//div[@id='Android']//table//tr") @iOSFindBy(xpath="//div[@id='iPhone']//table//tr") public static List<WebElement> filterItemsList; @AndroidFindBy(id="submitBtn") @iOSFindBy(id="purchaseBtn") public static WebElement buyButton; //This element isn't on Android @iOSFindBy(id="searchBtn") public static WebElement searchButton; public ShoppingHomePageImpl(WebDriver driver, DeviceType deviceType) { super(driver, deviceType); } @Override public boolean isLoaded() { boolean loaded = actions.isDisplayed(searchInputField, 5); return loaded; } public SeleniumMobileBasePage searchForItem(String searchTerm) { actions.type(searchTerm, searchInputField); if(deviceType.equals(DeviceType.Android)) { //Android automatically filters the list as soon as something is entered. return new ShoppingHomePageImpl(driver, DeviceType.Android); } else { //iOS must click the search button. actions.click(searchButton); return new ShoppingHomePageImpl(driver, DeviceType.iOS); } } public BuyItemPageInterface buyItem(String exactItemName) { actions.click(getElementFromFilteredList(exactItemName)); actions.click(buyButton); return new BuyItemPageImpl(driver, deviceType); } /** Will return null if no match is found, or if input exactItemText=null * Otherwise will return the WebElement matching the exactItemText from the filtered list * @param exactItemText * @return */ protected WebElement getElementFromFilteredList (String exactItemText) { if(exactItemText == null) { return null; } for (WebElement item : filterItemsList) { if(exactItemText.contains(actions.getText(item))) { return item; } } return null; } }
SeleniumMobileBasePage
public abstract class SeleniumMobileBasePage extends SeleniumPageObject{ protected DeviceType deviceType; public SeleniumMobileBasePage(WebDriver driver, DeviceType deviceType) { super(driver); this.deviceType = deviceType; if(!isLoaded()) { throw new SeleniumNavigationException("Page " + this.getClass().getSimpleName() + " was not loaded in the browser when trying to construct the object.", driver); PageFactory.initElements(driver, this); } } }
The OS Specific FindBy Annotations allow us to declare a different way of finding the same conceptual object between the different OSes. This solves our first issue mentioned in the Background.
We can also use the
deviceType
variable to write condition statements, like the one in our searchForItem()
method. Based on the device type, we can perform one behavior, or another. This solves our second issue we mentioned in the Background.From here, it should be reasonably clear how we can then construct a test case that looks something like this:
ShoppingTest
public class ShoppingTest extends CorporateMobileBaseTest{ ShoppingHomePageInterface shoppingHomePage; @Before public void setupLoginTests() { shoppingHomePage = (ShoppingHomePageInterface) landingPage; } @Test public void testLoginSuccess() { String itemName = "banana"; String itemCost = "$0.67"; shoppingHomePage.searchForItem("ban"); BuyItemPageInterface buyPage = shoppingHomePage.buyItem(itemName); Assert.assertEquals(itemName, buyPage.getItemBeingBought()); Assert.assertEquals(itemCost, buyPage.getAmountOfItem()); ConfirmationPageInterface confirmPage = buyPage.submitOrder(); Assert.assertEquals(itemName, confirmPage.getItemThatWasBought()); Assert.assertEquals(itemCost, confirmPage.getAmountOfItem()); Assert.assertTrue(confirmPage.isConfirmNumberPresent()); confirmPage.logConfirmationNumber(); } }
We can assume CorporateMobileBaseTest simply launches our Android or iOS sim/emulator (or real device) and then launches our app returning us a
landingPage
variable representing the first screen (or PageObject) of our application. We then use only Interfaces when inside our Test Case, since we’ll only know at run time if we’re dealing with the Android or iOS implementations of those interfaces.
In the end of this cheating section, you can see we’ve achieved our goal. A single test case which can be run on Android or iOS. So what’s wrong here?
Explaining why the Appium Page Factory breaks design patterns
There are 2 key programming patterns that we’re in very clear violation of with this design. Single Responsibility Principle – which states that a class or method should do only 1 thing, and do it well.
We’re actually in violation of this one twice! Open/Closed Principle – which states that code should be open to extension, but closed to modification. Basically that you should be able to add things like a new
deviceType
while only having to change something like a factory class, and not everywhere all throughout your code base.
Single Responsibility Principle Violations
This pattern, arguably the most important pattern in good computer programming, is what allows us to separate concerns. If you’ve ever had an “A broke B” defect before, it’s typically because a class (or method) is so big that when you make that one small change over here, there’s a downstream impact within that class (or method) somewhere else. But because that class (or method) is so big, and doing so much, the downstream impact is too hard to realize. While these example classes (and methods) are still small enough to see everything pretty clearly, in the real world, these Page Objects can get very big, and separating concerns where we can may end up saving hours, days, weeks, or more on trying to debug “flaky” or “failing” tests later on. In our ShoppingHomePageImpl class level above example, we have our class responsible for two things.- Figuring out how to locate objects.
- Implementations of user actions.
If we look back to an example from my prior blog where we were dealing with Web Page Selenium Page Objects (for a web version of our app), you could make the same argument there too. However, I think where I personally draw the line, is in that example (illustrated below), we’re basically assigning constants. Whereas in our new example (from above), we have to use Appium Annotations, which are really doing a lot of complicated logic behind the scenes for us.
ShoppingHome WebPageObject
public class ShoppingHome extends SeleniumStartPage{ private static final By searchBar = By.id("searchInpt"); private static final By searchButton = By.id("serachBtn"); private static final By itemList = By.id("filterItems"); private static final By purchaseButton = By.id("submitBtn"); ...
While one could argue that even these constants belong in a separate class to isolate concerns, I think we can all agree that this violation becomes significantly more egregious in our mobile space where the Appium Annotations add further logic to what’s being done in our WebElement/By object’s definitions. In addition, we also have a method in our example,
searchForItem()
which is clearly doing 2 different things:
- One set of instructions in the Android case.
- One set of instructions in the iOS case.
This type of smell alerts us that there should clearly be two distinct searchForItem() methods like searchForItemIos() and searchForItemAndroid(). However with this Appium PageFactory pattern, there’s no easy way for the Page Object, or the Test Case to know which of these two methods to call without having an
if(deviceType == Android/iPhone)
check like we currently have.
Risks for Appium Page Object for violating Single ResponsibilityWhen making updates to a locator for Android or iOS, you may inadvertently update the wrong one, or you may update a locator that has a much wider implication than you realize because your class is doing too much.
In addition, when updating a method that has different behavior between OSes, you may very easily impact an OS you didn’t intend to since all that logic is clumped together in the same method.
Open/Closed Principle Violations
Determining if adding a new “Thing” to your code is easy or hard, depends on if your project has implemented the Open/Closed Principle or not. It should always be easy to add another item to a list/enum and then support that new item within the codebase. Risks for Appium Page Object for violating Open/ClosedLets pretend we wanted to add a WindowsPhone app, Blackberry app, or even just wanted to add our Web page app to the same test suite, since the user requirement is the same on all of those. Another common example could be if you have an iPad or Android tablet app that’s slightly different in any behavior implementation from the phone apps, but still has the same overall user requirements and you wanted to add that/those to this test suite as well. From our example above, we’d need to scan EVERY ONE of our Page Objects and see where we are making
if(deviceType==DeviceType.SomeDevice)
, and now add in logic for our new DeviceType(s) as well. There’s no easy way to identify which PageObjects might have that kind of logic in them, so we need to carefully scan each one. Not to mention that now we’re violating the Single Responsibility Principle even further now. Instead of our method doing 2 things for Android and iOS, now it may be doing 5 for Web, Android Phone, Android Tablet, iPhone, and iPad!!
That’s a lot of logic all in one method!!
Fixing the Violations Using Our OpenSource Framework
In order to better separate concerns, we’re going to divide the one or two classes per screen that we had in our “Cheating” Appium Page Factory section into 5 different sets of classes.- Interface Classes to document what user actions are possible on each page.
- Common Locator Classes to store references to any conceptual objects that are common to all deviceTypes. We can also use this class to set the value of any locators that happen to have the same exact locator strategy across the different deviceTypes (if there are any).
- Abstract Implementation Classes to store any logic that is identical between all the different deviceTypes.
- OS specific Locator Classes to set the values of the various locators based on the specific OS this class belongs to. We can also add any locators to this class that only exist on this specific OS, and are not common to the other OS(es).
- OS specific full Implementation Page Object Classes that extend the common abstract versions of the Page Object, but also implement any user action methods that have OS specific behavior.
Interface Example
This class would actually be identical to what we’d have even using our Cheating Appium Page Factory pattern. It just declares the user actions that are possible on this PageObject.ShoppingHomeInterface
public interface ShoppingHomePageInterface extends MobileBaseInterface{ MobileBaseInterface searchForItem(String searchTerm); BuyItemPageInterface buyItem(String exactItemName); }
Common Locator Class Example
We create By objects that we will reference within our PageObject action methods.ShoppingHomeLocatorsImpl
public abstract class ShoppingHomeLocatorsImpl extends Locators { protected By searchInputField; protected By filterItemList; protected By buyButton; public void setSearchInputField() { //Both iOS and Android are using the same ID for this this.searchInputField = By.id("searchInpt"); } //The rest of the locators are OS specific public abstract void setFilterItemList(); public abstract void setBuyButton(); public By getSearchInputField() { return searchInputField; } public By getFilterItemList() { return filterItemList; } public By getBuyButton() { return buyButton; } }
Note: The Locators class we extend is part of the Open Source Framework [link coming soon]. This class just allows the framework to do some fancy logic to retrieve all the setters and call them to set all the appropriate values when we create the new Mobile Page Object instance.
Abstract Implementation Class Example
First, lets take a look at the MobileBaseInterface class which all our abstract implementations will be extending from.SeleniumMobileBasePage
public abstract class SeleniumMobileBasePage<T extends Locators> extends SeleniumPageObject implements MobileBaseInterface{ protected T locators; public SeleniumMobileBasePage(WebDriver driver) { super(driver); ParameterizedType superClass = (ParameterizedType) getClass().getGenericSuperclass(); @SuppressWarnings("unchecked") Class<T> type = (Class<T>) superClass.getActualTypeArguments()[0]; try { this.locators = type.newInstance(); } catch (Exception e) { throw new RuntimeException("Invalid Locator type of: " + type + "\n" + e.getMessage()); } assignValuesToAllLocators(); if(!isLoaded()) { throw new SeleniumNavigationException("Page " + this.getClass().getSimpleName() + " was not loaded in the browser when trying to construct the object.", driver); } } /** * Searches for all PageObjects in the project that implement the given interface, * And also reference the current deviceType. Then returns a new instance of the page that matches. * @param mobileInterface * @return */ public <K extends MobileBaseInterface> K getPageFromInterface(Class<K> mobileInterface) { ...
Some of this, especially the generics (T and K) might seem a little complicated. Lets go over what this framework class does.
The
<T extends Locators>
in the page declaration just means that when we create a new instance of a full class that extends this SeleniumMobileBasePage, that we have to tie some Locators class to it (like the OS specific version of the Locators for that OS specific version of the full class). This will become more clear when we get to the OS Specific Full Class Example section. Likewise the protected T locators
means that we have a variable locators
which will have to be of some type extending a Locators class.
The code lines after the call to super(driver); are simply getting the exact type of Locator that we tied to this class, and instanciating a new object of that type for our locators
object. Then our assignValuesToAllLocators()
method will call all the appropriate setters for our locators object setting all their values so we can use them throughout our PageObject code.
Lastly, the it’s important to note that this framework class gives us the ability to get an instance of a full Page Object class, just by passing in an Interface class. We aren’t going to go into how this is done in this article, just be aware that you have the option to do this on any of our Page Objects by extending this class.
Now lets look at our ShoppingHomePageImpl, which is our common implementation code which extends this SeleniumMobileBasePage (like all our Appium Page Objects do). We’ll go through this class in stages.ShoppingHomePageImpl declaration
public abstract class ShoppingHomePageImpl<T extends ShoppingHomeLocatorsImpl> extends SeleniumMobileBasePage<T> implements ShoppingHomePageInterface {
The first thing to note is that this is an
abstract
class. Which means it’s only a partial class. It’s stating that even though we’re implementing ShoppingHomeInterface, because we’re an abstract class, we reserve the right to not necessarily implement all the methods that interface says we need to here in this class.
We’re also tying the ShoppingHomeLocatorsImpl class to our Locators, while at the same time saying that any full implementation of this class must declare an even lower level Locators class which extends ShoppingHomeLocatorsImpl. Doing this will allow us to utilize any of the common locators in this common implementation class (but not any locators that might be OS specific).
These next set of methods should look very familiar from my prior blog. It’s just the constructor and isLoaded() method:
ShoppingHomePageImpl required methods
public ShoppingHomePageImpl(WebDriver driver) { super(driver); } @Override public boolean isLoaded() { boolean loaded = actions.isDisplayed(locators.getSearchInputField(), 5); return loaded; }
locators.getSearchInputField()
here because that’s something that’s in the ShoppingHomeLocatorsImpl that we tied to our class definition.
Lastly in this common class example, we have the implementation for any user action methods that are common across ALL deviceTypes.
ShoppingHomePageImpl common user actions
@Override public BuyItemPageInterface buyItem(String exactItemName) { actions.click(getElementFromFilteredList(exactItemName)); actions.click(locators.getBuyButton()); return getPageFromInterface(BuyItemPageInterface.class); } protected WebElement getElementFromFilteredList (String itemText) { if(itemText == null) { return null; } WebElement tableOfFilterItems = finder.getElement(locators.getFilterItemList()); WebElement cellToClick = tableHelper.getCellOverFromPartialStringMatch(tableOfFilterItems, itemText, 0, 0); return cellToClick; }
Our
getElementFromFilteredList
mainly uses a framework call from tableHelper
which lets us get the specific cell we want that contains the itemName.Also note our use of our framework method here for
getPageFromInterface()
We’re able to simply pass the Interface class we want returned, and the framework at runtime will determine if it needs to pass the Android or iOS full implementation of the BuyItemPage.
OS Specific Locator Classes Example
Here are the Android and iOS full Locator Classes:ShoppingHomeLocatorsAndroid
public class ShoppingHomeLocatorsAndroid extends ShoppingHomeLocatorsImpl { @Override public void setFilterItemList() { filterItemList = By.xpath("//div[@id='Android']//table"); } @Override public void setBuyButton() { buyButton = By.xpath("//div[@id='Android']//button"); } }
ShoppingHomeLocatorsIos
public class ShoppingHomeLocatorsIos extends ShoppingHomeLocatorsImpl { protected By searchButton; public void setSearchButton() { searchButton = By.xpath("//div[@id='iPhone']//button[@id='searchBtn']"); } public By getSearchButton() { return this.searchButton; } @Override public void setFilterItemList() { filterItemList = By.xpath("//div[@id='iPhone']//table"); } @Override public void setBuyButton() { buyButton = By.xpath("//div[@id='iPhone']//button[@id='purchaseBtn']"); } }
setFilterItemList()
and setBuyButton()
methods.
The Ios version of the class must also declare it’s OS specific locator for searchButton that isn’t present on the Android app at all.
OS Specific Full Implementation Classes Example
Let’s review each of the Android and iOS full Implementation classes now.ShoppingHomePageAndroid
public class ShoppingHomePageAndroid extends ShoppingHomePageImpl<ShoppingHomeLocatorsAndroid> implements AndroidMobileInterface{ public ShoppingHomePageAndroid(WebDriver driver) { super(driver); } @Override public ShoppingHomePageAndroid searchForItem(String searchTerm) { //For Demo Purposes, lets type 1 char at a time. char[] charList = searchTerm.toCharArray(); for (char character : charList) { actions.type(String.valueOf(character), locators.getSearchInputField()); Demo.wait(1); } //Android automatically filters the list as soon as something is entered. return new ShoppingHomePageAndroid(driver); } }
From the class declaration, we can see that we are extending the common Implementation class
ShoppingHomePageImpl
, and since that was implementing the ShoppingHomePageInterface
, we’ll need to implement any leftover methods from that interface class that the common abstract Impl class didn’t already implement for us (aka, any user action methods that have OS specific behavior), like our searchForItem()
method. Also from our class declaration, we can see that this class implements the AndroidMobileInterface
. This is a Framework Interface signifying that this full class is of type ANDROID.
Lastly from the class declaration, we are finally tying the ShoppingHomeLocatorsAndroid
, or Android version of the locators to this Android implementation of the class.
ShoppingHomePageIos and ShoppingSelectionPageIos
public class ShoppingHomeSearchPageIos extends ShoppingHomePageImpl<ShoppingHomeLocatorsIos> implements IphoneMobileInterface{ public ShoppingHomeSearchPageIos(WebDriver driver) { super(driver); } @Override public MobileBaseInterface searchForItem(String searchTerm) { actions.type(searchTerm, locators.getSearchInputField()); //iOS must click the search button. actions.click(locators.getSearchButton()); //Verify the Item Selection page loads properly return new ShoppingHomeItemSelectionPageIos(driver); } } public class ShoppingHomeItemSelectionPageIos extends SeleniumMobileBasePage<ShoppingHomeLocatorsIos> implements IphoneMobileInterface{ public ShoppingHomeItemSelectionPageIos(WebDriver driver) { super(driver); } @Override public boolean isLoaded() { boolean loaded = actions.isDisplayed(locators.getFilterItemList(), 5); return loaded; } }
This code is nearly identical to the Android code. Only the iOS code needs to click an additional ‘Search’ button, and then land on a selection page that we want to verify correctly loads before moving on (presumably to buy the item) next. Note that while the Android class tied the
ShoppingHomeLocatorsAndroid
to it’s class, the Ios class is tying the ShoppingHomeLocatorsIos
to it. This is what enables us to use the locators.getSearchButton()
in our Ios implementation class. If we tried to make that same call in the Android class, it wouldn’t know about that locator since it’s only part of the Ios Locators class.
Lastly, similar to the Android side, this class implements the IphoneMobileInterface
which is another framework interface signifying that this full implementation class is an IPHONE type.
The Test Case Example
Following the same procedures for both the BuyPage and ConfirmationPage, we can create a test case which is actually identical to what we saw from the Appium PageFactory example:ShoppingTest
public class ShoppingTest extends CorporateMobileBaseTest{ ShoppingHomePageInterface shoppingHomePage; @Before public void setupLoginTests() { shoppingHomePage = (ShoppingHomePageInterface) landingPage; } @Test public void testLoginSuccess() { String itemName = "banana"; String itemCost = "$0.67"; shoppingHomePage.searchForItem("ban"); BuyItemPageInterface buyPage = shoppingHomePage.buyItem(itemName); Assert.assertEquals(itemName, buyPage.getItemBeingBought()); Assert.assertEquals(itemCost, buyPage.getAmountOfItem()); ConfirmationPageInterface confirmPage = buyPage.submitOrder(); Assert.assertEquals(itemName, confirmPage.getItemThatWasBought()); Assert.assertEquals(itemCost, confirmPage.getAmountOfItem()); Assert.assertTrue(confirmPage.isConfirmNumberPresent()); confirmPage.logConfirmationNumber(); } }
At runtime, our CorporateMobileBaseTest will launch our application on an Android device/emulator or iPhone device/simulator and bring us to our ShoppingHome Page on the appropriate device. Then we can proceed using the Interface methods in our test which will either call the method implemented in our common Impl class if the behavior is the same between both deviceTypes, or the method implemented in the OS specific full class if the behavior is OS specific.
Conclusion
By utilizing our opensource framework [link coming soon], and the patterns in the latter section of this blog, we can actually augment and improve our test suite easily if the need arises in the future. If we needed to create an iPad version of our app, with the same user requirements for example, we’d simply need to create an iPadMobileInterface class, and then *LocatorsIpad and *PageIpad classes where all the iPad specific objects and logic would go. There’s no need to inspect and/or update any of the existing classes in our project code at all, any new code would only go in our new Ipad classes helping us ensure that adding code to accommodate the iPad app wouldn’t break any of our Android or iPhone test runs! Also, if/when functionality changes in the future, we can easily add or remove locators from the appropriate places without worrying how those locators might be utilized in the PageObject classes due to their separation. Our Page Object classes are kept as small as possible containing only the code needed to implement common actions (for our common Impl classes), or the code needed to complete any OS specific behavior (in our full OS specific classes). I hope you’ve found this information, and our base framework [link to Vanguard GitHub when available] useful. Please post any comments you have below.Posted in OpenSource