1. 程式人生 > >JUnit 5和Selenium基礎(二)

JUnit 5和Selenium基礎(二)

使用Selenium內建的PageFactory實現頁面物件模式

在這一部分中,將通過Selenium的內建PageFactory支援類來介紹Page Object模式的實現。PageFactory提供一種機制來初始化任何宣告WebElementList<WebElement>帶有@FindBy註釋的欄位的Page Object

由於不可描述的原因,我已經將測試網頁打包,需要的請留意文末資訊。

介紹頁面物件模式

頁面物件模式的目標是從實際測試中抽象出應用程式頁面和功能。頁面物件模式提高了程式碼在測試和固定裝置之間的可重用性,但也使程式碼易於維護。

頁面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無關。因此,從理論上講,我們可以針對不同的裝置(例如移動本機應用程式,桌面應用程式和Web應用程式)使用此頁面的不同實現。

建立測試

定義了頁面API後,可以直接跳轉到建立測試方法。在確認API可用於建立測試之後,再進行頁面實現。這種設計模式使測試人員可以專注於應用程式的實際使用,而不必太早掉進細節的坑裡。

建立了以下測試:

@ExtendWith(SeleniumExtension.class)
@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")
    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")
    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")
    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());
    }
}

在上述測試類中,我們看到在每次測試之前,ChromeDriver均已@BeforeEach通過Selenium Jupiter副檔名(@ExtendWith(SeleniumExtension.class))初始化並注入到設定方法中。驅動程式物件將用於初始化頁面物件。

頁面物件模式很大程度上取決於專案的特徵。你可能要經常使用介面,但這不是必需的。你可能要考慮在較低的抽象水平,其中API是暴露的更詳細的方法,例如setTodoInput(String value)clickSubmitButton()

使用Selenium內建的PageFactory實現Page Object Pattern

我們已經有一個介面可以對TodoMVC頁面的行為進行建模,並且我們有使用API​​的失敗測試。下一步是實際實現頁面物件。為此,我們將使用Selenium內建PageFactory類及其實用程式。

PageFactory類簡化了頁面物件模式的實現。該類提供了一種機制來初始化任何宣告WebElementList<WebElement>帶有@FindBy註釋的欄位的Page ObjectPageFactory中提供了支援Page Object模式實現的和其他註釋。

下面的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("***");
    }
 
    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!"));
    }
 
    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標籤。在這個例子中,硒將搜尋元件與class = "button"是內與元件id = "menu":

@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物件

PageFactory提供了幾種靜態方法來初始化Page Objects。在我們的測試中,在beforeEach()方法中,我們需要初始化TodoMvcPage物件:

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

PageFactory使用反射初始化物件,然後將其初始化所有WebElementList<WebElement>標有欄位@FindBy註釋。使用此方法要求Page Object具有單個引數建構函式接受WebDriver物件。

定位元素

那麼元素何時定位?每次訪問該欄位都會進行查詢。例如,當我們執行程式碼:new TodoInput.sendKeys(todoName + Keys.ENTER);in 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
 
demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
 
demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED
 
demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED
 
demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED
 
demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED
 
BUILD SUCCESSFUL in 27s
3 actionable tasks: 3 executed

微信公眾號後臺回覆“測試網頁”,獲取測試網頁下載地址。


  • 鄭重宣告:文章首發於公眾號“FunTester”,禁止第三方(騰訊雲除外)轉載、發表。

技術類文章精選

  • java一行程式碼列印心形
  • Linux效能監控軟體netdata中文漢化版
  • 效能測試框架第二版
  • 如何在Linux命令列介面愉快進行效能測試
  • 圖解HTTP腦圖
  • 將swagger文件自動變成測試程式碼
  • 基於java的直線型介面測試框架初探
  • Selenium 4.0 Alpha更新日誌
  • Selenium 4.0 Alpha更新實踐
  • 如何統一介面測試的功能、自動化和效能測試用例

非技術文章精選

  • 為什麼選擇軟體測試作為職業道路?
  • 寫給所有人的程式設計思維
  • 成為自動化測試的7種技能
  • 如何在DevOps引入自動化測試
  • Web端自動化測試失敗原因彙總
  • 如何在DevOps引入自動化測試
  • 測試人員常用藉口
  • API測試基礎
  • API自動化測試指南
  • 未來的QA測試工程師