功能性 Web 測試的未來?

工程 | Peter Ledbrook | 2010年8月28日 | ...

Groovy 社群是一個高產的群體,這意味著有大量的框架、庫和工具可以讓您的生活更輕鬆。測試領域似乎特別肥沃,我最近一直在研究兩種工具,當它們結合使用時,有望在您編寫功能性 Web 測試時帶來生產力的飛躍。

儘管我通常關注的是 Grails,但你無需使用 Grails 也能從這些工具中受益:它們適用於任何 Web 應用程式,並且能與任何基於 Java 的專案/構建良好整合。碰巧的是,它們都關聯了外掛,使得在 Grails 中使用它們變得相當簡單。

我想討論的第一個工具是 Spock。它基於行為驅動開發 (BDD) 正規化,將焦點從測試本身轉移到從預期行為的角度思考程式碼。你編寫的測試用例讀起來像規範,這不僅使它們更容易閱讀和理解,也更容易編寫。你甚至可以將 Spock 整合到任何 Java 專案中,並從你的 IDE(只要 IDE 支援 Groovy——三大主流 IDE 都支援)執行你的規範。

第二個工具甚至更新。它叫做 Geb,它使用 WebDriver 透過真實瀏覽器或 HtmlUnit 庫來測試 Web 應用程式。Geb 之所以脫穎而出,是因為它具有類似 jQuery 的語法,用於查詢 HTML 頁面,並且內建支援 頁面物件模式

那麼,為什麼我認為它們是成功的組合呢?因為它們讓編寫功能性 Web 測試變得儘可能簡單!讓我們看看它們如何協同工作。

一個簡單的例子

想象一下,你有一個簡單的登入頁面需要測試。它接受使用者名稱和密碼,並有一個“登入”按鈕。HTML 看起來像這樣:

<html> 
<head> 
  <title>Login</title>
</head> 
<body>
  <form action="/wildcard-realm/auth/signIn" method="post" > 
    <input type="hidden" name="targetUri" value="" /> 
    <table> 
      <tbody> 
        <tr> 
          <td>Username:</td> 
          <td><input type="text" name="username" value="" /></td> 
        </tr> 
        <tr> 
          <td>Password:</td> 
          <td><input type="password" name="password" value="" /></td> 
        </tr> 
        <tr> 
          <td>Remember me?:</td> 
          <td>
            <input type="hidden" name="_rememberMe" />
            <input type="checkbox" name="rememberMe" id="rememberMe"  />
          </td> 
        </tr> 
        <tr> 
          <td /> 
          <td><input type="submit" value="Sign in" /></td> 
        </tr> 
      </tbody> 
    </table> 
  </form>
</body> 
</html> 

現在,看看下面的 Spock 規範,試著弄清楚它正在測試什麼行為:

import geb.spock.GebReportingSpec
import pages.*
 
class MySpec extends GebReportingSpec {
    String getBaseUrl() { "https://:8080/wildcard-realm" }
    File getReportDir() { new File("target/reports/geb") }

    def "Test invalid password"() {
        given: "I'm at the login page"
        to LoginPage

        when: "I enter an invalid password for 'admin'"
        loginForm.username = "admin"
        loginForm.password = "sdfkjhk"
        signIn.click()

        then: "I'm redirected back to the login page with the password field empty and an error message"
        at LoginPage
        loginForm.username == "admin"
        !loginForm.password
        message.text() == "Invalid username and/or password"
    }

    def "Test valid login"() {
        given: "I'm at the login page"
        to LoginPage

        when: "I enter a valid username and password"
        loginForm.username = "admin"
        loginForm.password = "admin"
        signIn.click(HomePage)

        then: "I'm redirected to the home page, which displays my username"
        at HomePage
        $().text().contains("Welcome back admin!")
    }
}

我不知道你怎麼樣,但我發現很容易弄清楚這個測試試圖做什麼。即使你在這個階段不知道變數是從哪裡來的,你也可以有效地將規範作為自然語言來閱讀。這種易於理解性是像 Spock 這樣的 BDD 工具的一大優勢。

讓我們更詳細地看一下這個規範。每個測試方法(或者 Spock 喜歡稱之為“特性”方法)都分解為幾個部分。第一個部分,given,包含任何設定程式碼,併為你提供測試的起始狀態。然後你宣告一個when塊,它在你正在測試的任何內容中啟動一些行為,例如透過提交表單。最後,你在then塊中檢查刺激的結果,該塊包含你需要充分驗證預期行為的條件。與 JUnit 測試不同,你不需要在then部分內進行顯式斷言,因為每個表示式都是隱式斷言。

這是一個簡單的概念,但一旦你習慣了編寫規範,你會發現 Spock 讓編寫測試變得更容易。這是我無法真正解釋的事情。我最好的猜測是,語法和結構與你在頭腦中構思測試的方式相匹配,因此思考要測試什麼與編寫實際測試用例之間幾乎沒有阻礙。但與其聽信我的話,我敦促你嘗試一下。你可以使用 Spock 進行單元測試和功能測試,所以很容易上手。

測試中的其他所有內容都屬於 Geb,包括to()還是at()方法。這兩個方法都作用於頁面物件,你需要自己編寫。幸運的是,這足夠簡單,正如你可以從LoginPage類中看出的那樣:

package pages

import geb.Page

class LoginPage extends Page {
    static url = "auth/login"

    static at = { title == "Login" }

    static content = {
        loginForm { $("form") }
        message { $("div.message") }
        signIn { $("input", value: "Sign in") }
    }
}

讓我們分別看看這個類中的靜態屬性:

  • url- 頁面的相對 URL;由to()方法用於確定將 HTTP 請求傳送到哪個 URL。
  • at- 一個閉包,指示當前頁面是否是此頁面——由at()方法呼叫;它應該返回一個布林值,但你也可以包含斷言。
  • content- 頁面內容的描述,允許輕鬆訪問此處宣告的部分。

因此在上面的例子中,你可以看到登入頁面的相對 URL 是 “auth/login”。相對於什麼?相對於測試的baseUrl。判斷當前頁面是否是登入頁面,只需在at閉包中檢查頁面標題是否為“Login”即可。最後,content塊提供了對登入表單(頁面上唯一的表單)、資訊/錯誤訊息“div”和“登入”按鈕的直接訪問——所有這些都透過 Geb 的$()方法實現。

如果你回頭看測試,你會發現我可以像訪問測試的屬性一樣訪問內容元素,例如loginForm。Geb 的這一特性允許編寫非常簡潔和自描述的測試,但更重要的是,它促進了程式碼重用。想象一下你的 HTML 頁面發生變化,其中一個表示式不再匹配你想要的內容。如果你沒有使用頁面物件,你將不得不執行一個可能不可靠的全域性搜尋和替換。相反,在頁面物件中更改那個引用會好多少!

$()功能不僅限於content塊——如果你願意,你可以直接從測試程式碼中使用它。考慮這個測試:

    ...
    def "Test authentication redirect with query string"() {
        when: "I access the book list page with a sort query string"
        login "admin", "admin", BookListPage, [sort: 'title', order: 'desc']

        then: "The list of books is displayed in the correct order"
        at BookListPage
        $("tbody tr").size() == 3
        $("tbody tr")*.find("td", 1)*.text() == [ "Misery", "Guns, Germs, and Steel", "Colossus" ]
    }
    ...
    /**
     * Logs into the application either via a target page that requires
     * authentication or by directly requesting the login page.
     */
    private login(username, password, targetPage = null, params = [:]) {
        if (targetPage) {
            to([*:params], targetPage)
            page LoginPage
        }
        else {
            to LoginPage
        }

        loginForm.username = username
        loginForm.password = password

        if (targetPage) signIn.click(targetPage)
        else signIn.click(HomePage)
    }
    ...

類似 jQuery 的語法使得匹配 HTML 頁面中的元素並提取屬性值和內容變得非常容易。它的語法以及 Geb 的許多其他功能在 Geb 手冊 中有詳細介紹。

前面的例子也演示瞭如何將程式碼從測試中抽取出來,放入可重用方法中。因為 Spock 的語法起初相當陌生,你可能不認為這有可能。但規範最終是類,所以你可以像對待類一樣對待它們。

我可以一直講 Spock 和 Geb 的功能以及如何使用它們,但本文不是教程。它更像是一個讓你感興趣的嚐鮮。如果你想檢視我從中提取上述片段的完整 Spock 規範,請檢視它的原始碼以及相關的頁面物件

你還在等什麼?

Spock 和 Geb 目前都還很年輕(都未達到 1.0 版),但它們已經發展到人們積極將其應用於專案中的程度。即使在這個階段,它們也展現出了引人注目的優勢:功能性 Web 測試既相對易於編寫又易於理解。

這可不是小事。自動化功能測試對於確保 Web 應用程式正常執行至關重要,但當前的方法(至少在 Java 生態系統中)通常笨拙且阻礙了這些測試的編寫。因此,團隊最終依賴手動測試,而手動測試永遠無法提供你真正需要的可靠覆蓋率。

一切都盡善盡美嗎?當然不是。但我們這裡有兩款工具,它們讓原本應該容易測試的東西,真正變得容易測試——這在功能性 Web 測試領域絕非易事。並且,隨著你需要測試的頁面變得更加複雜,它們將繼續為你提供支援。也許最大的問題是處理 Javascript 並從測試中觸發 DOM 事件,但即使在那方面,你也會發現 Geb 正在迅速發展以幫助你。

我甚至還沒有提及 Spock 內建的模擬框架,或者它對資料驅動測試的支援(請查閱專案文件中的where子句)。就連它的斷言輸出也是一大優點:

dateService.getMonthString(new Date().updated(month: month)) == expected
|           |              |          |              |       |  |
|           June           |          |              5       |  July
|                          |          |                      false
|                          |          |                      2 differences (50% similarity)
|                          |          |                      Ju(ne)
|                          |          |                      Ju(ly)
|                          |          Sun Jun 27 12:24:02 BST 2010
|                          Fri Aug 27 12:24:02 BST 2010
org.grails.util.DateService@527f58ef

在 Geb 方面,可以看看模組功能,它允許你將頁面物件分解為可重用部分。非常適合複雜的頁面。由於它底層使用 WebDriver,因此你既可以用真實瀏覽器執行測試,也可以在無頭模式下(透過 HtmlUnit)執行。

最後,我重申這些工具在 Java 專案中與在 Groovy 或 Grails 專案中同樣適用。僅僅因為你的 Web 應用程式是用 Java 編寫的,並不意味著你的測試也必須如此。還要明白,Spock 可以用於任何型別的 Java 專案——用於單元測試、整合測試和/或功能測試。

如果說我學到了一件事,那就是編寫測試必須儘可能容易,否則它們根本就不會被編寫。這就是為什麼我認為 Geb 和 Spock 是測試領域的重要發展,值得深入研究。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有