Appium PageObject

时间:2024-04-22 07:38:40

原文地址http://blog.****.net/TalorSwfit20111208/article/details/77434950

由于无法联系上您,在此分享您的文章,希望谅解!

Appium PageObject 直接沿用了Selenium的PageObject设计模式,

PageObject主要优点如下:

一、将UI元素与逻辑分离方便后期维护

二、减少代码冗余

三、增强代码可读性

来看个例子

没有使用PO设计模式的代码如下:

  1. @Test
  2. public void twoPlusTwoOperation() {
  3. /* 获取控件*/
  4. MobileElement buttonTwo = (MobileElement)(driver.findElement(By.id("net.ludeke.calculator:id/digit2")));
  5. MobileElement buttonPlus = (MobileElement)(driver.findElement(By.id("net.ludeke.calculator:id/plus")));
  6. MobileElement buttonEquals = (MobileElement)(driver.findElement(By.id("net.ludeke.calculator:id/equal")));
  7. MobileElement resultField = (MobileElement)(driver.findElement(By.xpath("//android.widget.EditText[1]")));
  8. /* 计算2+2*/
  9. buttonTwo.click();
  10. buttonPlus.click();
  11. buttonTwo.click();
  12. buttonEquals.click();
  13. /* 检查在给定的时间内是否显示要查找的控件 */
  14. new WebDriverWait(driver, 30).until(ExpectedConditions.textToBePresentInElement(resultField, EXPECTED_RESULT_FOUR));
  15. }

使用了PO设计模式的代码如下

  1. @Test
  2. public void twoPlusTwoOperation() {
  3. app.calculatorScreen().addTwoAndTwo();
  4. assertTrue(app.calculatorScreen().isResultCorrect("4"));
  5. }

我们注意到的第一个最直接的变化是测试方法的长度。使用PageObject模式编写的测试方法几乎总是比原始的方法短(对于较长的测试而言更短)。如果你继续阅读,你会注意到,这不仅是因为我们在addTwoAndTwo方法中包装了所有的按钮。
可读性怎么样?再次通过这两种方法,问问自己在哪种情况下更容易理解发生了什么。另外,请注意我们如何在第二种方法中真的不需要注释,因为指定与Page Object具有的交互的方法具有重要的名称。
通过将低级操作包含在专用方法中,我们现在有了不直接引用任何WebDriver API的测试方法。在编写第一个PageObject测试方法时,请使用缺少引用低级API的导入语句作为根据模式进行处理的指标。
这种方法给了我们另一个不容忽视的优点:通过隐藏单一实用程序方法的技术复杂性,PageObject模式使得用户交互的流程变得明显。对于更长,更复杂的测试,以及我们编写测试的整个方式的转换,这特别有用。一旦实现了应用程序屏幕的基本交互,编写测试方法基本上只是通过调用正确名称所指的方法来复制用例。这就是为什么你应该努力为他们选择最好的名字。

PageObject控件定位

pageobject控件定位是使用注解方式来定位的,如下

# WebElement/列表 WebElement 字段可以这样定位:

使用@FindBy注解

  1. import org.openqa.selenium.support.FindBy;
  2. import org.openqa.selenium.WebElement;
  3. @FindBy(someStrategy)//用来定位浏览器或者webview UI
  4. //也可以用来定位native 应用,当没有定义其他定位策略时
  5. WebElement someElement;@FindBy(someStrategy) //用来定位浏览器或者webview UI
  6. //也可以用来定位native 应用,当没有定义其他定位策略时
  7. List<WebElement> someElements;//定位包含相同控件属性的控件

使用@AndroidFindBy来定位

  1. import io.appium.java_client.android.AndroidElement;
  2. import org.openqa.selenium.remote.RemoteWebElement;
  3. import io.appium.java_client.pagefactory.*;
  4. @AndroidFindBy(someStrategy) //用Android UI Automator来定位Android UI
  5. AndroidElement someElement;
  6. @AndroidFindBy(someStrategy) //用Android UI Automator来定位Android UI
  7. List<AndroidElement> someElements;

多种组合查找策略

  1. import org.openqa.selenium.remote.RemoteWebElement;
  2. import io.appium.java_client.pagefactory.*;
  3. import org.openqa.selenium.support.FindBy;
  4. import org.openqa.selenium.support.FindByAll;
  5. import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE;
  6. @HowToUseLocators(androidAutomation = ALL_POSSIBLE, iOSAutomation = ALL_POSSIBLE)
  7. @FindAll{@FindBy(someStrategy1), @FindBy(someStrategy2)})
  8. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2)
  9. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  10. RemoteWebElement someElement;
  11. @HowToUseLocators(androidAutomation = ALL_POSSIBLE, iOSAutomation = ALL_POSSIBLE)
  12. @FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)})
  13. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2)
  14. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  15. List<RemoteWebElement> someElements;

## 也可以用下面这种方式: 

  1. import org.openqa.selenium.remote.RemoteWebElement;
  2. import io.appium.java_client.pagefactory.*;
  3. import org.openqa.selenium.support.FindBy;
  4. import org.openqa.selenium.support.FindByAll;
  5. import static io.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN;
  6. import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE;
  7. @HowToUseLocators(androidAutomation = CHAIN, iOSAutomation = ALL_POSSIBLE)
  8. @FindAll{@FindBy(someStrategy1), @FindBy(someStrategy2)})
  9. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2)
  10. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  11. RemoteWebElement someElement;
  12. @HowToUseLocators(androidAutomation = CHAIN, iOSAutomation = ALL_POSSIBLE)
  13. @FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)})
  14. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2)
  15. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  16. List<RemoteWebElement> someElements;

或者

  1. import org.openqa.selenium.remote.RemoteWebElement;
  2. import io.appium.java_client.pagefactory.*;
  3. import org.openqa.selenium.support.FindBy;
  4. import org.openqa.selenium.support.FindByAll;
  5. import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE;
  6. @HowToUseLocators(iOSAutomation = ALL_POSSIBLE)
  7. @FindAll{@FindBy(someStrategy1), @FindBy(someStrategy2)})
  8. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) //this is the chain
  9. //by default
  10. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  11. RemoteWebElement someElement;
  12. @HowToUseLocators(iOSAutomation = ALL_POSSIBLE)
  13. @FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)})
  14. @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) //this is the chain
  15. //by default
  16. @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2)
  17. List<RemoteWebElement> someElements;

应用PO查找

# Appium Java client使用了AppiumFieldDecorator来融合了Selenium PageFactory

对象字段结构如下:

  1. import io.appium.java_client.pagefactory.*;
  2. import org.openqa.selenium.support.PageFactory;
  3. PageFactory.initElements(new AppiumFieldDecorator(searchContext
  4. /*searchContext is a WebDriver or WebElement
  5. instance */),
  6. pageObject //对象类的一个实例
  7. );
  1. import io.appium.java_client.pagefactory.*;
  2. import org.openqa.selenium.support.PageFactory;
  3. import java.util.concurrent.TimeUnit;
  4. PageFactory.initElements(new AppiumFieldDecorator(searchContext,
  5. /*searchContext is a WebDriver or WebElement
  6. instance */
  7. 15, //默认为所有查找策略的隐式等待时间
  8. TimeUnit.SECONDS),
  9. pageObject //对象类的一个实例
  10. );
  1. import io.appium.java_client.pagefactory.*;
  2. import org.openqa.selenium.support.PageFactory;
  3. import java.util.concurrent.TimeUnit;
  4. PageFactory.initElements(new AppiumFieldDecorator(searchContext,
  5. /*searchContext is a WebDriver or WebElement
  6. instance */
  7. new TimeOutDuration(15, //默认为所有查找策略的隐式等待时间
  8. TimeUnit.SECONDS)),
  9. pageObject //对象类的一个实例
  10. );

来看个经典计算器程序在PageObject中的实际应用

项目结构
安装程序的核心组件将是以下类:

  • AbstractTest:我们在其中设置测试阶段;
  • AppiumDriverBuilder:设置所需的功能并实例化驱动程序;
  • 我们调用的应用程序类访问我们的Screen对象;
  • AbstractScreen,包含您的Screen对象之间的所有共享方法;
  • Screen类包含表示用户与被测试应用程序交互的方法;
  • 测试类包含一个或多个测试,写成屏幕方法调用序列。

为了进一步澄清项目结构,您可以将这些类组织成包。在一个实用程序包中,您可以包括您的AppiumDriverBuilder类以及您创建的其他实用程序类。您也可以将AbstractScreen类和其他Screen类放在屏幕包中。项目结构如下:

Appium PageObject

AbstractTest
在项目的中心是AbstractTest类。这里我们定义我们的测试套件方法,它将在每次测试运行之前执行。这里我们做两件非常重要的事情:

  1. 负责初始化负责连接到Appium服务器的驱动程序;
  2. 实例化了App类,这将允许我们访问我们想要测试的应用程序的单个屏幕;
  1. package com.test.calculatortest;
  2. import com.test.calculatortest.Calculator;
  3. import com.test.calculatortest.util.AppiumDriverBuilder;
  4. import io.appium.java_client.AppiumDriver;
  5. import org.junit.Before;
  6. import java.net.MalformedURLException;
  7. import java.net.URL;
  8. public abstract class AbstractTest {
  9. private AppiumDriver<?> driver;
  10. protected Calculator app;
  11. /* Establish a connection to TestObject, or to a local device test is local. */
  12. @Before
  13. public void connect() throws MalformedURLException {
  14. this.driver = AppiumDriverBuilder.forAndroid()
  15. .withEndpoint(new URL("http://127.0.0.1:4723/wd/hub"))
  16. .build("com.android.calculator2", ".Calculator");
  17. //实例化应用类
  18. app = new Calculator(driver);
  19. }
  20. }

AppiumDriverBuilder
它基本上是一个支持类,负责配置和实例化的Appium驱动程序。

  1. package com.test.calculatortest.util;
  2. import io.appium.java_client.AppiumDriver;
  3. import io.appium.java_client.android.AndroidDriver;
  4. import io.appium.java_client.android.AndroidElement;
  5. import org.openqa.selenium.remote.DesiredCapabilities;
  6. import java.net.URL;
  7. public abstract class AppiumDriverBuilder<SELF, DRIVER extends AppiumDriver<?>> {
  8. public static AndroidDriverBuilder forAndroid() {
  9. return new AndroidDriverBuilder();
  10. }
  11. public static class AndroidDriverBuilder extends AppiumDriverBuilder<AndroidDriverBuilder, AndroidDriver<?>> {
  12. DesiredCapabilities capabilities = new DesiredCapabilities();
  13. @Override
  14. public AndroidDriver<?> build(String appPackage,String appActivity) {
  15. capabilities.setCapability("platformName", "Android");
  16. //使用Android模拟器
  17. capabilities.setCapability("deviceName", "testDevice");
  18. //使用Android模拟器
  19. capabilities.setCapability("platformVersion", "4.4.4");
  20. //不重新安装应用
  21. capabilities.setCapability("noReset",true);
  22. //待测包名及首次启动的页面
  23. capabilities.setCapability("appPackage", appPackage);
  24. capabilities.setCapability("appActivity", appActivity);
  25. //使用appium Unicode键盘输入法,输入完毕后重置输入法
  26. capabilities.setCapability("unicodeKeyboard", true);
  27. capabilities.setCapability("resetKeyboard", true);
  28. capabilities.setCapability("deviceReadyTimeout",30);
  29. return new AndroidDriver<AndroidElement>(endpoint, capabilities);
  30. }
  31. }
  32. protected URL endpoint;
  33. @SuppressWarnings("unchecked")
  34. public SELF withEndpoint(URL endpoint) {
  35. this.endpoint = endpoint;
  36. return (SELF) this;
  37. }
  38. public abstract DRIVER build(String appPackage,String appActivity);
  39. }

应用类
测试中的另一个中心类将是Application类(我们简单的命名为我们正在测试的应用程序的名称)。这个类的功能是提供屏幕(正如我们之前所说的,Page对象)到需要访问它们的功能的方法(屏幕类中的测试方法)。

  1. package com.test.calculatortest;
  2. import com.test.calculatortest.screen.CalculatorScreen;
  3. import io.appium.java_client.AppiumDriver;
  4. /*
  5. * 应用类,返回各个操作页面类
  6. */
  7. public class Calculator {
  8. private final AppiumDriver<?> driver;
  9. public Calculator(AppiumDriver<?> driver) {
  10. this.driver = driver;
  11. }
  12. public CalculatorScreen calculatorScreen() {
  13. return new CalculatorScreen(driver);
  14. }
  15. }

AbstractScreen类

AbstractScreen类将包含Screen对象之间共享的所有方法。这些可能是通用目的的方法,可以执行多个点(滑动,滚动)与应用程序交互所需的手势,这些手段隐藏了一些更为复杂的代码,从而增加了测试方法的可读性,同步方法等。

  1. package com.test.calculatortest.screen;
  2. import io.appium.java_client.AppiumDriver;
  3. import io.appium.java_client.MobileElement;
  4. import io.appium.java_client.pagefactory.AppiumFieldDecorator;
  5. import org.openqa.selenium.By;
  6. import org.openqa.selenium.OutputType;
  7. import org.openqa.selenium.support.PageFactory;
  8. import org.openqa.selenium.support.ui.ExpectedConditions;
  9. import org.openqa.selenium.support.ui.WebDriverWait;
  10. public abstract class AbstractScreen {
  11. protected final AppiumDriver<?> driver;
  12. public AbstractScreen(AppiumDriver<?> driver) {
  13. this.driver = driver;
  14. PageFactory.initElements(new AppiumFieldDecorator(driver), this);
  15. }
  16. public MobileElement findElementWithTimeout(By by, int timeOutInSeconds) {
  17. return (MobileElement)(new WebDriverWait(driver, timeOutInSeconds)).until(ExpectedConditions.presenceOfElementLocated(by));
  18. }
  19. protected void takeScreenShot(){
  20. driver.getScreenshotAs(OutputType.BASE64);
  21. }
  22. }

注意
PageFactory.initElements(new AppiumFieldDecorator(driver), this);这句,这可以让你使用注释来抓取UI元素,因此请勿忘记将其包含在您的设置中!

屏幕类
屏幕类代表应用程序的屏幕。在这里获取UI元素并与代表可能与用户界面交互的方法与其进行交互,例如打开菜单并选择项目,填写某些字段并按下提交按钮,向下滚动列表并选择正确的元素这样,你的测试方法将只是不同屏幕上的一系列用户交互。这将使你的测试易于维护和扩展。

  1. package com.test.calculatortest.screen;
  2. import io.appium.java_client.AppiumDriver;
  3. import io.appium.java_client.MobileElement;
  4. import io.appium.java_client.pagefactory.AndroidFindBy;
  5. import org.openqa.selenium.TimeoutException;
  6. import org.openqa.selenium.support.ui.ExpectedConditions;
  7. import org.openqa.selenium.support.ui.WebDriverWait;
  8. public class CalculatorScreen extends AbstractScreen {
  9. @AndroidFindBy(id = "com.android.calculator2:id/digit2")
  10. private MobileElement buttonTwo;
  11. @AndroidFindBy(id = "com.android.calculator2:id/plus")
  12. private MobileElement buttonPlus;
  13. @AndroidFindBy(id = "com.android.calculator2:id/equal")
  14. private MobileElement buttonEquals;
  15. @AndroidFindBy(xpath = "//android.widget.EditText[1]")
  16. private MobileElement resultField;
  17. public CalculatorScreen(AppiumDriver<?> driver) {
  18. super(driver);
  19. }
  20. public void addTwoAndTwo() {
  21. buttonTwo.click();
  22. buttonPlus.click();
  23. buttonTwo.click();
  24. buttonEquals.click();
  25. }
  26. public boolean isResultCorrect(String result) {
  27. try {
  28. /* Check if within given time the correct result appears in the designated field. */
  29. (new WebDriverWait(driver, 30)).until(ExpectedConditions.textToBePresentInElement(resultField, result));
  30. return true;
  31. } catch (TimeoutException e) {
  32. return false;
  33. }
  34. }
  35. }

除了刚刚描述的应用程序的显着“主屏幕”之外,我们还可以创建另一个表示计算器应用程序的“高级面板”的程序,这基本上是自己的屏幕。在此屏幕中引用的UI元素将是计算器的符号/函数。
你可以为应用程序的每个屏幕创建一个Screen对象,也可以决定仅对真正重要的屏幕执行此操作。这两种方法都有其优点和缺点,但请记住,可以使用太多屏幕的应用程序来处理,另一方面,在一个Screen对象中抽取太多的屏幕可能会导致混乱。
测试类
您的测试按照扩展AbstractTest的类进行分组。这允许您抓住应用程序的任何屏幕,并通过您编写的方法与其进行交互。

  1. package com.test.calculatortest;
  2. import org.junit.Test;
  3. import static org.junit.Assert.assertTrue;
  4. /*
  5. *
  6. * 逻辑操作类
  7. *
  8. */
  9. public class OperationTests extends AbstractTest {
  10. public OperationTests() {}
  11. /* 一个简单的加法运算,期望结果为正确的值 */
  12. @Test
  13. public void twoPlusTwoOperation() {
  14. app.calculatorScreen().addTwoAndTwo();
  15. assertTrue(app.calculatorScreen().isResultCorrect("4"));
  16. }
  17. }

将计算器的例子放在一边,并跳入一个现实世界的例子:

  1. public class ChatTest extends AbstractTest {
  2. @Test
  3. public void sendMessageAndCheckHistoryTest() {
  4. login(Credentials.VALID_USER_CREDENTIALS);
  5. app.mainScreen().startChatWithUser(TEST_USERNAME);
  6. app.chatScreen().sendChatMessage(TEST_MESSAGE);
  7. app.chatScreen().navigateToHistoryScreen();
  8. assertTrue(app.historyScreen().containsMessage(TEST_MESSAGE));
  9. }
  10. @Test
  11. public void sendAndDeleteMessageThenCheckHistoryTest() {
  12. ...
  13. }
  14. }

正如你所看到的,当涉及多个屏幕时,这种模式的目的变得清晰,方便起见。我们现在正在浏览我们从未见过的一系列屏幕,但是我们已经可以得到我们测试中发生了什么的一般概念。如果我们看看我们调用的屏幕方法的实现,我们将会更准确地了解发生了什么。事实上,我们可以在没有这样做的情况下收集一些信息是使用PageObject编写测试的好处之一。
如果UI发生小的变化,我们可能不需要改动我们的测试方法:改动将在我们的屏幕方法之一发生。在这种变化频繁的敏捷环境中,除了测试脚本的强大性外,还特别受欢迎。
您可以选择自己的屏幕方法的复杂程度。拥有更多,更简单的屏幕方法将导致更长,更详细的测试方法,暴露更多的交互的复杂性。按照这种方法,上述方法看起来更像这样:

  1. @Test
  2. public void sendMessageAndCheckHistoryTest() {
  3. login(Credentials.VALID_USER_CREDENTIALS);
  4. app.mainScreen().navigateToUserSelection();
  5. app.userSelectionScreen().selectUser(TEST_USERNAME);
  6. app.userProfileScreen().startChat();
  7. app.chatScreen().sendChatMessage(TEST_MESSAGE);
  8. app.chatScreen().navigateToMainScreen();
  9. app.mainScreen().navigateToHistoryScreen();
  10. assertTrue(app.historyScreen().containsMessage(TEST_MESSAGE));
  11. }

虽然这种方法显示了屏幕之间的每个过渡,但是它可能很容易成为压倒性的,如在这个例子中:

  1. public class CreateDocumentationWithSuggestionTest extends AbstractTest {
  2. @Test
  3. public void buildNewDocumentationWithSuggestions() {
  4. app.documentationScreen().navigateToSettings();
  5. app.settingsScreen().navigateToSuggestions();
  6. app.settingsScreen().activateSuggestions(SUGGESTIONS));
  7. app.settingsScreen().navigateToDocumentation();
  8. app.documentationScreen().createDocumentation();
  9. app.documentationCreationScreen().selectCultivation();
  10. app.documentationDetailsScreen().selectFields(TEST_CULTIVATION_1.getFields());
  11. app.documentationDetailsScreen().selectConsumables(TEST_CULTIVATION_1.getConsumables());
  12. app.documentationDetailsScreen().selectWorkers(TEST_CULTIVATION_1.getWorkers());
  13. app.documentationDetailsScreen().sendActivity();
  14. app.documentationScreen().createDocumentation();
  15. app.documentationCreationScreen().selectCultivation();
  16. Assert.assertTrue(app.documentationDetailsScreen().areSuggestedFieldsFilledOut(TEST_CULTIVATION_1));
  17. }
  18. ...
  19. }

您应该保持测试方法足够短,以便您能够一目了然地告诉他们做什么,而不用将所有内容都包装到单一屏幕方法中。寻找平衡是编写一个好的,可维护的测试套件的关键。
总结:
PageObject前期可能工作量有点多,但是后面都是照葫芦画瓢非常容易维护,所以使用起来性价比还是挺高的,相比直来直去的测试脚本也减少了大量的重复代码