Appium is a well-known OSS test automation tool for mobile applications. This time I would like to summarize how to write a PageObject pattern in that Appium.
Do you know the Page Objects pattern?
According to the Selenium HQ Page Objects page
- The public methods represent the services that the page offers
is what it reads. Translated with my understanding
In other words, I understand that PageObjects are objects that represent the relationship between a page and a user (regardless of the test).
If you want to know more about the merits, you should purchase and read the following. Selenium Design Patterns & Best Practices
https://github.com/appium/java-client/blob/master/docs/Page-objects.md The features described in are very helpful in writing Page Objects.
I would like to summarize it briefly here.
For example, if you specify an Android Element, you can get it by specifying as follows. (The someStrategy
part contains the method for specifying Elements such as id and xpath. The same applies thereafter.)
It's very easy! !!
@AndroidFindBy(someStrategy)
AndroidElement someElement;
Appium also supports iOS, Android, Web and WebView. It would be convenient if we could support Cross Platform by changing only the Element specification method! ??
What a convenient thing you can do!
For example, when specifying iOS and Android, it will be like this.
@AndroidFindBy(someStrategy)
@iOSFindBy(someStrategy)
MobileElement someElement;
Annotate each of iOS and Android, and specify MobileElement
of the class that can be supported by both OSs.
What if you want to include the Web in addition to both operating systems?
@FindBy(someStrategy) //for browser or web view html UI
@AndroidFindBy(someStrategy) //for Android native UI
@iOSFindBy(someStrategy) //for iOS native UI
RemoteWebElement someElement;
In this way, annotate both OS and Web, and specify the corresponding RemoteWebElement
.
The relationship between the OS and the class is as follows.
OS | class |
---|---|
iOS | IOSElement |
Android | AndroidElement |
Web | WebElement |
iOS,Android | MobileElement |
iOS,Android,Web | RemoteWebElement |
Chain,All possible Now that you can specify the Element, What if you want to specify an Element within a particular Element? What if there are multiple Elements you want to specify using different specification methods? Is it specified by xpath even if it becomes complicated?
A solution is provided for this.
@HowToUseLocators(androidAutomation = CHAIN, iOSAutomation = ALL_POSSIBLE)
@AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2)
@iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
RemoteWebElement someElement;
As you can see in the atmosphere, if you specify it with CHAIN
, it becomes an AND condition (to be exact, an Element specified by someStrategy2
under the Element specified by someStrategy1
), and if it is ʻALL_POSSIBLE`, it becomes an OR condition. I will.
The default is CHAIN
, so if you specify only CHAIN
, you don't have to write @HowToUseLocators
.
Widget
Looking at the actual screen, there are things that are common to some screens, right? For example, you may want to use the header in common.
In that case, define a class that inherits Widget.
@AndroidFindBy(someStrategy)
public class Header extends Widget {
public Header(WebElement element) {
super(element);
}
@AndroidFindBy(someStrategy)
private MobileElement back;
}
You need to define a constructor and call the parent constructor. The method of specifying Element is the same as usual. The annotations in the class specify the common parts. If the method of specifying the common part is different for each screen, specify the annotation when defining the variable widget in PageObjects.
Now that we've looked at annotations and widgets, let's look at how to define actual PageObjects.
First, let's take a look at plain Page Objects.
public class SamplePageObject {
public Header header;
@AndroidFindBy(someStrategy)
private MobileElement hogehoge;
}
In fact, just generating this class leaves Element as null
.
In order to substitute the actual Element, it is necessary to describe the following processing after creating PageObjects.
SamplePageObject pageObject = new SamplePageObject();
PageFactory.initElements(new AppiumFieldDecorator(webdriver,
pageObject //an instance of PageObject.class
);
This will assign the Element of the actual screen to Elements.
Actually, what I have introduced so far is the content described in wiki of appium java-client. Yes, the wiki doesn't describe how to implement the actual PageObjects pattern.
So, from now on, I would like to introduce an implementation method that makes the most of the Page Objects pattern that I have come up with.
First, create a base PageObjects class so that Element is specified when PageObjects is created.
public abstract class PageObject {
protected WebDriver driver;
public PageObject(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);
}
//Common processing if necessary
}
A class that inherits this class will be assigned to Element simply by calling the parent constructor with WebDriver
as an argument.
public class TopPage extends PageObject {
public TopPage(WebDriver driver) {
super(driver);
}
//Element specification
}
As I wrote at the beginning
Public methods are the logical processing provided by the page
Represents.
Looking at the actual screen, many processes involve screen transitions. So I think it's a good idea to implement the method so that it returns the next screen.
public class TopPage extends PageObject {
public TopPage(WebDriver driver) {
super(driver);
}
@AndroidFindBy(id = "jp.co.android.sample:id/hogehoge")
private MobileElement hogehoge;
@AndroidFindBy(id = "jp.co.android.sample:id/refresh_button")
private MobileElement refresh;
public HogeHogePage moveToHogeHoge() {
hogehoge.click();
return new HogeHogePage(this.driver);
}
public TopPage refresh() {
refresh.click();
return this;
}
}
Since WebDriver
is needed in such a case, I saved it in the constructor of the parent class.
Also, if the screen does not transition, it is better to return itself.
This is influenced by Geb, but it is better to define a method to check if the screen transition is correct after the screen transition. thinking about.
It will be verified in the constructor of the parent class.
public abstract class PageObject {
protected WebDriver driver;
public PageObject(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);
if(!at()) {
throw new RuntimeException("The specified screen transition could not be performed.:" + this.getClass());
}
}
protected abstract boolean at();
}
If the screen transition fails, you may feel that it is an error and it is forcible, If the screen transition is not performed correctly, an error will occur in the subsequent processing. I think this is kinder than the error message at that time.
This ʻat` method is implemented in a child class. Normally, there should be a fixed element on the screen (such as a title), so it's a good idea to compare its existence and text. If not, I think it's okay to always return true at worst.
How do you guys do the validation for the PageObjects pattern? For example, what if you want to see if an Element with a screen is displayed?
I don't want to have such a method on the PageObjects side. That's because, as I said at the beginning, I understand that PageObjects are objects that represent the relationship between a page and a user (regardless of testing). (Users don't verify, right?)
Then what do you do? I don't think there is a correct answer to this.
I think one solution is to define the Element as a public field and use it for validation.
However, you have to be careful, as described in this issues, the element of the widget is called the public method. Since it is assigned when, it will be null
otherwise.
The answer so far in me is that the method for validation is defined in PageObjects as minimally as possible.
First of all, why did you come to this conclusion?
null
, so I don't want the Element to be a public field.That's why.
So how do you do that? In my case, I generated a validation method in the parent class.
public abstract class PageObject {
protected WebDriver driver;
public PageObject(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);
}
public boolean isDisplayed(String fieldName) {
Class aClass = this.getClass();
try {
Field declaredField = null;
while (aClass != null) {
try {
declaredField = aClass.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
aClass = aClass.getSuperclass();
}
}
if (declaredField == null) {
throw new NoSuchFieldException();
}
declaredField.setAccessible(true);
Object field = declaredField.get(this);
if (field instanceof MobileElement) {
MobileElement element = (MobileElement) field;
return element != null && element.isDisplayed();
}
throw new NoSuchFieldException("The specified Field is not defined as a MobileElement. :" + fieldName);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("The specified Field is not defined. :" + fieldName);
}
}
}
The advantage of this method is that it covers the above three reasons. On the contrary, the disadvantage is
That is the point.
However, my personal opinion about the first disadvantage is that if you increase the number of verification items too much, the maintenance cost will increase, so you do not have to do the verification itself as much as possible (the above-mentioned ʻat` method is sufficient). I think. However, I thought that it was always possible to verify whether the elements of the screen were displayed, so I defined it.
I often hear that Appium itself has become famous, I had a hard time because there were few Japanese articles about Appium java-client. I would be grateful if you could share information on how you are using java-client.
Also, Appium itself does not know what kind of language is mainstream. It would be helpful if you could tell me which language has the advantage.