功能性 Web 测试的未来?

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

Groovy 社区是一群高效的人,这意味着有大量的框架、库和工具可以让你更轻松。测试领域似乎是一片特别肥沃的土壤,我最近一直在研究几个工具,它们结合起来有望在编写功能性 Web 测试时显著提高你的生产力。

虽然我通常关注 Grails,但你不必使用 Grails 也能获得这些工具的好处:它们适用于任何 Web 应用程序,并且能很好地集成到任何基于 Java 的项目/构建中。碰巧的是,它们都有相关的插件,使得从 Grails 使用它们变得非常简单。

我想谈论的第一个工具是 Spock。它基于行为驱动开发 (BDD) 范式,将焦点从测试本身转移到根据 预期行为 来思考你的代码。你编写的测试用例就像规范一样,这不仅使它们更容易阅读和理解,也更容易编写。你甚至可以将 Spock 集成到任何 Java 项目中,并在 IDE 中运行你的规范(只要 IDE 支持 Groovy - 三个主要的 IDE 都支持)。

第二个工具更新一些。它叫做 Geb,它使用 WebDriver 来测试 Web 应用程序,可以使用真实浏览器或 HtmlUnit 库。Geb 与众不同之处在于其用于查询 HTML 页面的类似 jQuery 的语法以及对 页面对象模式 的内置支持。

那么为什么我认为这是一个成功的组合呢?因为它们让编写功能性 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() { "http://localhost: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,包含任何 setup 代码并给出测试的起始状态。然后你声明一个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()方法用于确定向哪个 URL 发送 HTTP 请求。
  • at- 一个闭包,指示当前页面是否是此页面 - 由at()方法调用;它应该返回一个布尔值,但你也可以包含断言。
  • content- 页面内容的描述,允许轻松访问在此处声明的部分。

因此在上面的例子中,你可以看到登录页面的相对 URL 是 "auth/login"。相对于什么?相对于测试的baseUrl。判断当前页面是否是登录页面,只需检查页面标题是否是 "Login",在at闭包中。最后,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 社区即将举行的所有活动。

查看全部