JUnit 5和Selenium –使用Selenium内置的`PageFactory`实现页面对象模式

Selenium是一组支持浏览器自动化的工具和库,主要用于Web应用程序测试。 Selenium的组件之一是Selenium WebDriver,它提供客户端库,JSON有线协议(与浏览器驱动程序进行通信的协议)和浏览器驱动程序。 Selenium WebDriver的主要优点之一是,它受所有主要编程语言的支持,并且可以在所有主要操作系统上运行。

带有Selenium WebDriverJUnit 5的这一部分中,我将通过Selenium的内置PageFactory支持类来介绍Page Object模式的实现。 PageFactory提供了一种机制,用于初始化任何声明使用@FindBy批注注释的WebElementList<WebElement>字段的Page对象。

关于本教程

您正在阅读带有Selenium WebDriverJUnit 5的第二部分-教程

本教程中的所有文章:

接下来的是:

  • 第3部分–改进项目配置–并行执行测试,测试执行顺序,参数化测试,AssertJ等

该教程的源代码可以在Github上找到

介绍页面对象模式

我们将在以下位置为基于JavaScript的Todo应用程序创建测试: http : //todomvc.com/examples/vanillajs 。 该应用程序被创建为单页应用程序(SPA),并使用本地存储作为任务存储库。 可能实现的方案包括添加和编辑待办事项,删除待办事项,将单个或多个待办事项标记为已完成。 该实现将使用Page Object模式完成。

页面对象模式的目标是从实际测试中抽象出应用程序页面和功能。 页面对象模式提高了代码在测试和固定装置之间的可重用性,但也使代码易于维护。

您可以在Martin Fowler的文章中了解有关此模式的更多信息: https : //martinfowler.com/bliki/PageObject.html

页面API或页面对象

我们将从将TodoMVC页面建模为Page Object的项目开始。 该对象将表示将在测试中使用的页面API。 可以使用接口对API本身进行建模。 如果查看以下界面的方法,则会注意到这些方法只是页面上可用的用户功能。 用户可以创建待办事项 ,用户可以重命名待办事项 ,也可以删除待办事项

 public interface TodoMvc { 
     void navigateTo(); 
     void createTodo(String todoName); 
     void createTodos(String... todoNames); 
     int getTodosLeft(); 
     boolean todoExists(String todoName); 
     int getTodoCount(); 
     List<String> getTodos(); 
     void renameTodo(String todoName, String newTodoName); 
     void removeTodo(String todoName); 
     void completeTodo(String todoName); 
     void completeAllTodos(); 
     void showActive(); 
     void showCompleted(); 
     void clearCompleted();  } 

上面的接口(显然)隐藏了所有实现细节,但也没有将任何Selenium WebDriver详细信息公开给潜在的客户端(在我们的情况下,客户端=测试方法)。 实际上,它与Selenium WebDriver无关。 因此,从理论上讲,我们可以针对不同的设备(例如移动本机应用程序,桌面应用程序和Web应用程序)使用此页面的不同实现。

创建测试

定义了页面API后,我们可以直接跳转到创建测试方法。 在确认API可用于创建测试之后,我们将进行页面实现。 这种设计技术使您可以专注于应用程序的实际使用,而不必太早进入实现细节。

创建了以下测试:

 @ExtendWith (SeleniumExtension. class )  @DisplayName ( "Managing Todos" @DisplayName "Managing Todos" )  class TodoMvcTests { 
     private TodoMvc todoMvc; 
     private final String buyTheMilk = "Buy the milk" ; 
     private final String cleanupTheRoom = "Clean up the room" ; 
     private final String readTheBook = "Read the book" ; 
     @BeforeEach 
     void beforeEach(ChromeDriver driver) { 
         this .todoMvc = null ; 
         this .todoMvc.navigateTo(); 
     } 
     @Test 
     @DisplayName ( "Creates Todo with given name" ) 
     void createsTodo() { 
         todoMvc.createTodo(buyTheMilk); 
         assertAll( 
                 () -> assertEquals( 1 , todoMvc.getTodosLeft()), 
                 () -> assertTrue(todoMvc.todoExists(buyTheMilk)) 
         ); 
     } 
     @Test 
     @DisplayName ( "Creates Todos all with the same name" @DisplayName "Creates Todos all with the same name" ) 
     void createsTodosWithSameName() { 
         todoMvc.createTodos(buyTheMilk, buyTheMilk, buyTheMilk); 
         assertEquals( 3 , todoMvc.getTodosLeft()); 
         todoMvc.showActive(); 
         assertEquals( 3 , todoMvc.getTodoCount()); 
     } 
     @Test 
     @DisplayName ( "Edits inline double-clicked Todo" ) 
     void editsTodo() { 
         todoMvc.createTodos(buyTheMilk, cleanupTheRoom); 
         todoMvc.renameTodo(buyTheMilk, readTheBook); 
         assertAll( 
                 () -> assertFalse(todoMvc.todoExists(buyTheMilk)), 
                 () -> assertTrue(todoMvc.todoExists(readTheBook)), 
                 () -> assertTrue(todoMvc.todoExists(cleanupTheRoom)) 
         ); 
     } 
     @Test 
     @DisplayName ( "Removes selected Todo" ) 
     void removesTodo() { 
         todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook); 
         todoMvc.removeTodo(buyTheMilk); 
         assertAll( 
                 () -> assertFalse(todoMvc.todoExists(buyTheMilk)), 
                 () -> assertTrue(todoMvc.todoExists(cleanupTheRoom)), 
                 () -> assertTrue(todoMvc.todoExists(readTheBook)) 
         ); 
     } 
     @Test 
     @DisplayName ( "Toggles selected Todo as completed" ) 
     void togglesTodoCompleted() { 
         todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook); 
         todoMvc.completeTodo(buyTheMilk); 
         assertEquals( 2 , todoMvc.getTodosLeft()); 
         todoMvc.showCompleted(); 
         assertEquals( 1 , todoMvc.getTodoCount()); 
         todoMvc.showActive(); 
         assertEquals( 2 , todoMvc.getTodoCount()); 
     } 
     @Test 
     @DisplayName ( "Toggles all Todos as completed" @DisplayName "Toggles all Todos as completed" ) 
     void togglesAllTodosCompleted() { 
         todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook); 
         todoMvc.completeAllTodos(); 
         assertEquals( 0 , todoMvc.getTodosLeft()); 
         todoMvc.showCompleted(); 
         assertEquals( 3 , todoMvc.getTodoCount()); 
         todoMvc.showActive(); 
         assertEquals( 0 , todoMvc.getTodoCount()); 
     } 
     @Test 
     @DisplayName ( "Clears all completed Todos" @DisplayName "Clears all completed Todos" ) 
     void clearsCompletedTodos() { 
         todoMvc.createTodos(buyTheMilk, cleanupTheRoom); 
         todoMvc.completeAllTodos(); 
         todoMvc.createTodo(readTheBook); 
         todoMvc.clearCompleted(); 
         assertEquals( 1 , todoMvc.getTodosLeft()); 
         todoMvc.showCompleted(); 
         assertEquals( 0 , todoMvc.getTodoCount()); 
         todoMvc.showActive(); 
         assertEquals( 1 , todoMvc.getTodoCount()); 
     }  } 

更多:如果您不熟悉JUnit 5,则可以在我的博客上阅读此介绍: https : //blog.codeleak.pl/2017/10/junit-5-basics.html 。 本文还有一个用波兰语写的较新版本: https : //blog.qalabs.pl/junit/junit5-pierwsze-kroki/

在上面的测试类中,我们看到在每次测试之前,ChromeDriver均已初始化,并通过Selenium Jupiter扩展名(因此@ExtendWith(SeleniumExtension.class) )注入到设置方法( @BeforeEach )中。 驱动程序对象将用于初始化页面对象。

页面对象建模技术不同,并且很大程度上取决于您正在处理的项目的特征。 您可能要使用接口,但这不是必需的。 您可能需要考虑在较低的抽象级别上进行建模,在该级别上,API公开了更详细的方法,例如setTodoInput(String value)clickSubmitButton()

使用Selenium内置的PageFactory实现Page Object Pattern

到现在为止,我们已经有一个接口可以对TodoMVC页面的行为进行建模,并且我们有使用API​​的失败测试。 下一步是实际实现页面对象。 为此,我们将使用Selenium内置的PageFactory类及其实用程序。

PageFactory类简化了Page Object模式的实现。 该类提供了一种机制,用于初始化任何声明使用@FindBy批注注释的WebElementList<WebElement>字段的Page Object。 可以在org.openqa.selenium.support包中找到PageFactory和支持页面对象模式实现的所有其他注释。

下面的TodoMvcPage类实现了我们之前创建的接口。 它声明了几个用@FindBy注释注释的字段。 它还声明一个构造函数,该构造函数采用工厂使用的WebDriver参数来初始化字段:

 public class TodoMvcPage implements TodoMvc { 
     private final WebDriver driver; 
     private static final By byTodoEdit = By.cssSelector( "input.edit" ); 
     private static final By byTodoRemove = By.cssSelector( "button.destroy" ); 
     private static final By byTodoComplete = By.cssSelector( "input.toggle" ); 
     @FindBy (className = "new-todo" ) 
     private WebElement newTodoInput; 
     @FindBy (css = ".todo-count > strong" ) 
     private WebElement todoCount; 
     @FindBy (css = ".todo-list li" ) 
     private List<WebElement> todos; 
     @FindBy (className = "toggle-all" ) 
     private WebElement toggleAll; 
     @FindBy (css = "a[href='#/active']" ) 
     private WebElement showActive; 
     @FindBy (css = "a[href='#/completed']" ) 
     private WebElement showCompleted; 
     @FindBy (className = "clear-completed" ) 
     private WebElement clearCompleted; 
     public TodoMvcPage(WebDriver driver) { 
         this .driver = driver; 
     } 
     @Override 
     public void navigateTo() { 
         driver.get( " http://todomvc.com/examples/vanillajs " ); 
     } 
     public void createTodo(String todoName) { 
         newTodoInput.sendKeys(todoName + Keys.ENTER); 
     } 
     public void createTodos(String... todoNames) { 
         for (String todoName : todoNames) { 
             createTodo(todoName); 
         } 
     } 
     public int getTodosLeft() { 
         return Integer.parseInt(todoCount.getText()); 
     } 
     public boolean todoExists(String todoName) { 
         return getTodos().stream().anyMatch(todoName::equals); 
     } 
     public int getTodoCount() { 
         return todos.size(); 
     } 
     public List<String> getTodos() { 
         return todos 
                 .stream() 
                 .map(WebElement::getText) 
                 .collect(Collectors.toList()); 
     } 
     public void renameTodo(String todoName, String newTodoName) { 
         WebElement todoToEdit = getTodoElementByName(todoName); 
         doubleClick(todoToEdit); 
         WebElement todoEditInput = find(byTodoEdit, todoToEdit); 
         executeScript( "arguments[0].value = ''" , todoEditInput); 
         todoEditInput.sendKeys(newTodoName + Keys.ENTER); 
     } 
     public void removeTodo(String todoName) { 
         WebElement todoToRemove = getTodoElementByName(todoName); 
         moveToElement(todoToRemove); 
         click(byTodoRemove, todoToRemove); 
     } 
     public void completeTodo(String todoName) { 
         WebElement todoToComplete = getTodoElementByName(todoName); 
         click(byTodoComplete, todoToComplete); 
     } 
     public void completeAllTodos() { 
         toggleAll.click(); 
     } 
     public void showActive() { 
         showActive.click(); 
     } 
     public void showCompleted() { 
         showCompleted.click(); 
     } 
     public void clearCompleted() { 
         clearCompleted.click(); 
     } 
     private WebElement getTodoElementByName(String todoName) { 
         return todos 
                 .stream() 
                 .filter(el -> todoName.equals(el.getText())) 
                 .findFirst() 
                 .orElseThrow(() -> new RuntimeException( "Todo with name " + todoName + " not found!" "Todo with name " " not found!" )); 
     } 
     private WebElement find(By by, SearchContext searchContext) { 
         return searchContext.findElement(by); 
     } 
     private void click(By by, SearchContext searchContext) { 
         WebElement element = searchContext.findElement(by); 
         element.click(); 
     } 
     private void moveToElement(WebElement element) { 
         new Actions(driver).moveToElement(element).perform(); 
     } 
     private void doubleClick(WebElement element) { 
         new Actions(driver).doubleClick(element).perform(); 
     } 
     private void executeScript(String script, Object... arguments) { 
         ((JavascriptExecutor) driver).executeScript(script, arguments); 
     }  } 

@FindBy不是唯一用于在Page Object中查找元素的注释。 还有@FindBys@FindAll

@FindBys

@FindBys批注用于标记Page Object上的字段,以指示查找应使用一系列@FindBy标记。 在此示例中,Selenium将 id = "menu"的元素搜索class = "button"的元素:

 @FindBys ({ 
   @FindBy (id = "menu" ), 
   @FindBy (className = "button" )  })  private WebElement element; 

@FindAll

@FindAll批注用于标记Page Object上的字段,以指示查找应使用一系列@FindBy标记。 在此示例中,Selenium将搜索所有class = "button"的元素以及所有id = "menu"的元素。 不保证元素按文档顺序排列:

 @FindAll ({ 
   @FindBy (id = "menu" ), 
   @FindBy (className = "button" )  })  private List<WebElement> webElements; 

PageFactory提供了几种静态方法来初始化Page Objects。 在我们的测试中,在beforeEach()方法中,我们需要初始化TodoMvcPage对象:

 @BeforeEach  void beforeEach(ChromeDriver driver) { 
     this .todoMvc = PageFactory.initElements(driver, TodoMvcPage. class ); 
     this .todoMvc.navigateTo();  } 

PageFactory使用反射初始化对象,然后初始化所有标有@FindBy批注的WebElementList<WebElement>字段( @FindBy不进行任何查找,而是对字段进行代理)。 使用此方法要求Page Object具有接受WebDriver对象的单个参数构造函数。

定位元素

那么元素何时定位? 每次访问该字段都会进行查找。 因此,例如,当我们执行代码时: newTodoInput.sendKeys(todoName + Keys.ENTER);createTodo()方法中,实际执行的指令是: driver.findElement(By.className('new-todo')).sendKeys(todoName + Keys.ENTER) 。 我们可以预料,不是在对象初始化期间而是在第一个元素查找期间引发未找到元素的潜在异常。

Selenium使用代理模式来实现所描述的行为。

@CacheLookup

在某些情况下,每次访问带注释的字段时都不需要查找元素。 在这种情况下,我们可以使用@CacheLookup批注。 在我们的示例中,输入字段在页面上没有更改,因此可以缓存其查找:

 @FindBy (className = "new-todo" )  @CacheLookup  private WebElement newTodoInput; 

运行测试

现在是执行测试的时候了。 可以从IDE或使用终端来完成:

 ./gradlew clean test --tests *TodoMvcTests 

通过所有测试,构建成功:

 > Task :test  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED  pl.codeleak.demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED  BUILD SUCCESSFUL in 27s  3 actionable tasks: 3 executed 

下一步

在本教程的下一部分中,您将学习如何改善项目配置。 您将学习并行执行测试,测试执行顺序,参数化测试,AssertJ等。

翻译自: https://www.javacodegeeks.com/2019/10/using-pagefactory-implement-page-object-pattern.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值