返回首页 Groovy 入门

Getting started

测试指南

1 简介

Groovy 对测试编写带有原生支持。除了语言特性以及与最先进的测试库及架构相集成,Groovy 生态系统内还诞生了大量的测试库及架构。

本章将先介绍语言的专有测试功能,接着将详细介绍 JUnit 集成,用于规范的 Spock,以及用于功能测试的 Geb。最后,再概述适用于 Groovy 的其他测试库。

2 语言特性

除了集成了对 JUnit 的支持外,Groovy 的一些原生功能已被证明非常适合测试驱动的开发。本节将进行相关介绍。

2.1 强力断言语句

编写测试意味着要指定假设,这就要使用断言。在 Java 中,这可以通过 assert 关键字(在 J2SE 1.4 中引入)来实现,assert 语句可以通过 JVM 参数 -ea(或 -enableassertions)和 -da(或 -disableassertions)来启用。断言语句在 Java 中默认是禁用的。

Groovy 的断言是 assert 的一种功能强大的变体,也被称为强力断言语句power assertion statement)。Groovy 的强力断言语句与 Java 的 assert 的区别在于输出中的布尔表达式会验证为 false

def x = 1
assert x == 2

// Output:   1⃣️          
//
// Assertion failed:
// assert x == 2
//        | |
//        1 false  

1⃣️ 表示标准错误输出

如果断言无法成功验证,则抛出 java.lang.AssertionError,包含一个原始异常消息的扩展版本。强力断言输出显示了从外在表达式到内在表达式的解析结果。

强力断言语句真正的能力可以体现在复杂的布尔语句中,以及与集合或其他可使用 toString 的类相关的语句中。

def x = [1,2,3,4,5]
assert (x << 6) == [6,7,8,9,10]

// Output:
//
// Assertion failed:
// assert (x << 6) == [6,7,8,9,10]
//         | |     |
//         | |     false
//         | [1, 2, 3, 4, 5, 6]
//         [1, 2, 3, 4, 5, 6]

另一个与 Java 所不同的是,Groovy 断言是默认启用的。出于语言设计的决策,去除了使断言无效的功能。或者,如同 Bertrand Meyer 所言:如果真的下水,最好带着游泳圈

另外一个值得注意的是,布尔表达式中带有一定副作用的方法。内部错误消息构建机制只能存储目标的实例引用,所以在遇到涉及副作用方法的情况时,错误消息文本在呈现时间上会出现异常。

assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]

// Output:
//
// Assertion failed:
// assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]
//                          |       |        |
//                          |       |        false
//                          |       [1, 2, 3, 4]
//                          [1, 2, 3, 4]           1⃣️

1⃣️ 错误消息显示的是集合的实际状态,而不是应用了 unique 方法之前的状态。

如果提供自定义断言错误消息,可以使用 Java 的 assert expression1 : expression2,其中 expression1 是布尔表达式,而 expression2 是自定义错误消息。注意,这样做会禁用强力断言,完全回退到自定义错误消息上。

2.2 模拟与存根

对一些模拟与存根方案,Groovy 提供了强大的内建支持。使用 Java 时,动态模拟框架是常用的,其关键原因就在于利用 Java 手动创建模拟是一种很繁重的工作。这样的框架可以轻松地应用于 Groovy 中,但创建自定义模拟显然更为轻松。利用简单的映射或闭包来创建自定义模拟。

下面就来介绍如何只利用 Groovy 的语言特性来创建模拟与存根。

2.2.1 映射强制

使用 map 或 expando,可以轻松包含协作对象(collaborator)的预期行为:

class TranslationService {
    String convert(String key) {
        return "test"
    }
}

def service = [convert: { String key -> 'some text' }] as TranslationService
assert 'some text' == service.convert('key.text')

as 操作符强制将映射转换为特定类。给出的映射键被解析为方法名,而映射值,groovy.lang.Closure 块,则被解析为方法代码块。

注意,如果用 as 操作符来处理 java.util.Map 的后代类,映射强制会产生妨碍。映射强制机制只针对特定的集合类,并没有考虑到自定义类。

2.2.2. 闭包强制

as 操作符能以一种简洁的形式用于闭包,从而非常适合开发者在简单环境下进行测试。虽然该技术还没有强大到能不再使用动态模拟的程度,但至少足以应付简单环境。

持有单一方法的类或接口,包括 SAM(单一抽象方法)的类,可以用于强制闭包,使其成为一种指定类型的对象。为了实现这种机制,Groovy 内部会创建一个指定类的代理子对象。因此对象不是指定类的直接实例。这一点是非常重要的,比如生成的代理对象元类后续被改动。

强制闭包成为指定类型对象的范例如下:

def service = { String key -> 'some text' } as TranslationService
assert 'some text' == service.convert('key.text')

Groovy 支持一种叫做隐式 SAM 强制的功能。这意味着 as 操作符并不一定会用在运行时能够推断目标 SAM 类型的情况下。这种强制非常适用于模拟整个 SAM 类。

abstract class BaseService {
    abstract void doSomething()
}

BaseService service = { -> println 'doing something' }
service.doSomething()
2.2.3. MockFor 和 StubFor

Groovy 的模拟及存根类位于 groovy.mock.interceptor 包中。

MockFor 类支持独立地对类进行测试(通常是单元测试),这是通过定义一种严格有序的协作对象行为来实现的。典型的测试情境通常会包含待测类及一个或多个协作对象。通常希望只对待测类的业务逻辑进行测试。为此,实现策略之一是通过简化的模拟对象来代替协作对象,以便隔离出测试目标内的逻辑。MockFor 类可以利用元编程来创建这样的模拟。协作对象的期望行为被定义为一种行为规范。行为会被自动强制执行并检查。

假设目标类如下所示:

class Person {
    String first, last
}

class Family {
    Person father, mother
    def nameOfMother() { "$mother.first $mother.last" }
}

利用 MockFor,模拟期望常常是序列相关的,自动会在结尾处调用 verify

def mock = new MockFor(Person)    1⃣️    
mock.demand.getFirst{ 'dummy' }
mock.demand.getLast{ 'name' }
mock.use {                         2⃣️ 
    def mary = new Person(first:'Mary', last:'Smith')
    def f = new Family(mother:mary)
    assert f.nameOfMother() == 'dummy name'
}
mock.expect.verify()              3⃣️    

1⃣️ 通过 MockFor 的一个新实例创建一个新模拟
2⃣️ Closure 被传入 use,启用模拟功能
3⃣️ 调用 verify 查看是否序列和方法调用数正如预期

StubFor 类支持独立地对类进行测试(通常是单元测试),允许定义的协作对象的期望次序松散loosely-ordered)。通常测试情境包括一个受测类以及一个或多个协作对象。这样的情境通常只希望测试 CUT 的业务逻辑。为此可以实施这样一种策略:利用简化的存根对象来代替协作实例,以便将目标类中的逻辑抽取出来。StubFor 允许使用元编程来创建这样的存根。协作对象的预期行为被定义为一种行为规范。

MockFor 不同的是,利用 verify 检查的存根期望是序列无关的,使用是可选的:

def stub = new StubFor(Person)       1⃣️    
stub.demand.with {                    2⃣️
    getLast{ 'name' }
    getFirst{ 'dummy' }
}
stub.use {                          3⃣️
    def john = new Person(first:'John', last:'Smith')
    def f = new Family(father:john)
    assert f.father.first == 'dummy'
    assert f.father.last == 'name'
}
stub.expect.verify()                4⃣️  

1⃣️ 通过 StubFor 新实例创建的一个新存根。
2⃣️ 使用 with 方法将所有闭包中的调用委托给 StubFor 实例。
3⃣️ Closure 传入 use,启用存根功能。
4⃣️ 调用 verify(可选的)检查是否调用数目符合预期。

MockForStubFor 无法应用于静态编译类,比如使用 @CompileStatic 的 Java 类或 Groovy 类。要想存根或模拟这些类,可以使用 Spock 或一种 Java 模拟库。

2.2.4 Expando 元类 (EMC)

Groovy 包含了一种特殊的 MetaClass:EMC(Expando 元类,ExpandoMetaClass)。允许使用简洁的闭包格式来动态添加方法、构造函数、属性、静态方法。

每个 java.lang.Class 都带有一个特殊的 metaclass 属性,该属性引用了一个 ExpandoMetaClass 实例。expando 元类并不局限于自定义类,它也可以用于 JDK 类,比如 java.lang.String

String.metaClass.swapCase = {->
    def sb = new StringBuffer()
    delegate.each {
        sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) :
            Character.toUpperCase(it as char))
    }
    sb.toString()
}

def s = "heLLo, worLD!"
assert s.swapCase() == 'HEllO, WORld!'

ExpandoMetaClass 是相当好的一种用于模拟功能的备选方案,可以实现一些更先进的事务,比如模拟静态方法。

class Book {
    String title
}

Book.metaClass.static.create << { String title -> new Book(title:title) }

def b = Book.create("The Stand")
assert b.title == 'The Stand'

或甚至构造函数:

Book.metaClass.constructor << { String title -> new Book(title:title) }

def b = new Book("The Stand")
assert b.title == 'The Stand'

模拟构造函数可能似乎是一种讨巧的方法,最好甚至不用考虑,但有效的用例也还是存在的,在 Grails 上就能找到一些范例:在运行时中,借助 ExpandoMetaClass 添加域类构造函数。域对象在 Spring 应用上下文中自我注册,并实现了由依赖项注入容器所控制的服务或 Bean 的注入。

如果希望改变每个测试方法级别上的 metaClass 属性,需要清除作用于元类上的更改,否则这些更改将持续作用于测试方法调用的整个过程之中。GroovyMetaClassRegistry 中替代元类就能去掉更改。

GroovySystem.metaClassRegistry.setMetaClass(java.lang.String, null)

注册 MetaClassRegistryChangeEventListener,跟踪改变的类,并清除选定的测试运行时的 cleanup 方法中的更改。可以在 Grails Web 开发框架中找到比较好的范例。

除了使用类级别的 ExpandoMetaClass,也支持使用元类或对象级别。

def b = new Book(title: "The Stand")
b.metaClass.getTitle {-> 'My Title' }

assert b.title == 'My Title'

在该例中,元类更改只与实例有关,根据测试情境,这可能要比全局元类更改适应性更好。

2.3 GDK 方法

下面将概述可以在测试用例情境(比如对于数据生成的测试)中使用的 GDK 方法。

2.3.1 Iterable##combinations

利用 java.lang.Iterable 兼容类中添加的 combinations 方法,可以从一个包含两个或更多子列表的列表中获得一个组合列表:

void testCombinations() {
    def combinations = [[2, 3],[4, 5, 6]].combinations()
    assert combinations == [[2, 4], [3, 4], [2, 5], [3, 5], [2, 6], [3, 6]]
}

该方法可以在测试用例情境下,针对特定的方法调用,生成所有可能的参数组合。

2.3.2 Iterable##eachCombination

利用添加到 java.lang.Iterable 兼容类中的 eachCombination 方法,如果组合是由 combinations 方法所构建的,那么它可以在每一个组合上应用一个函数(或者如同在该例中这样采用 groovy.lang.Closure)。

eachCombination 是一种添加到所有符合 java.lang.Iterable 接口的类上的 GDK 方法。它会在输入列表的每一个组合上应用一个函数:

void testEachCombination() {
    [[2, 3],[4, 5, 6]].eachCombination { println it[0] + it[1] }
}

该方法还可以用在测试上下文中,利用每一个生成的组合来调用方法。

2.4 工具支持

2.4.1 测试代码覆盖率

代码覆盖率是关于(单元)测试有效性的一种重要衡量标准。具有较高代码覆盖率的程序要比代码覆盖率较低的程序更安全,留存严重 bug 的几率要低得多。要想提示代码覆盖率,生成的字节码通常需要在执行前进行检测。Cobertura 就是受 Groovy 支持的用于此目的一款工具。

很多框架及构建工具都集成有 Cobertura。Grails 中有基于 Cobertura 的 code coverage plugin;Gradle 中则有 gradle-cobertura plugin。当然,它们仅仅是众多插件中的两个而已。

下例展示了如何在一个 Groovy 项目的 Gradle 构建脚本中启用 Cobertura 代码覆盖报告:

def pluginVersion = '<plugin version>'
def groovyVersion = '<groovy version>'
def junitVersion = '<junit version>'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.eriwen:gradle-cobertura-plugin:${pluginVersion}'
    }
}

apply plugin: 'groovy'
apply plugin: 'cobertura'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    testCompile "junit:junit:${junitVersion}"
}

cobertura {
    format = 'html'
    includes = ['**/*.java', '**/*.groovy']
    excludes = ['com/thirdparty/**/*.*']
}

Cobertura 代码覆盖报告和测试代码覆盖报告可以添加到持续集成构建任务中,可以为这些报告选择一些输出格式。

3 利用 JUnit 3 和 4 进行单元测试

Groovy 简化了 JUnit 测试,使其更具有 Groovy 的特点。下面就来探讨一下 JUnit 3/4 与 Groovy 的集成情况。

3.1. JUnit 3

或者在 Groovy 类中,最显著支持 JUnit 3 测试的一个类是 GroovyTestCase 类。由于派生自 junit.framework.TestCase,所以它提供了大量的额外方法,使 Groovy 测试变得易如反掌。

虽然 GroovyTestCase 继承自 TestCase,但这并不意味在项目中无法使用 JUnit 4 的一些特性。实际上,最近发布的一些 Groovy 版本都带有打包的 JUnit 4,并且带有对 TestCase 的后向支持实现。在 Groovy 邮件列表中的一些对是否使用 GroovyTestCase 或 JUnit 4 的讨论中,人们认为这种选择更多是由个人口味来决定的,但利用 GroovyTestCase ,你能免费使用大量的方法,便于编写特定类型的测试。

下面就来看看 GroovyTestCase 所提供的一些方法,完整的方法列表位于 [groovy.util.GroovyTestCase] (http://docs.groovy-lang.org/2.4.5/html/gapi/index.html?groovy/util/GroovyTestCase.html) 的 JavaDoc 文档中。另外,不要忘记它继承自 junit.framework.TestCase,后者继承了所有的 assert* 方法。

3.1.1 断言方法

GroovyTestCase 继承自 junit.framework.TestCase,因此也间接继承了大量的断言方法,从而可以应用到每一个测试方法中:

class MyTestCase extends GroovyTestCase {

    void testAssertions() {
        assertTrue(1 == 1)
        assertEquals("test", "test")

        def x = "42"
        assertNotNull "x must not be null", x
        assertNull null

        assertSame x, x
    }

}

如上所示,与 Java 不同,可以在大多数场合忽略括号,从而可以使 JUnit 断言方法调用表达式实现更好的可读性。

assertScript 是一种由 GroovyTestCase 添加的有趣方法,它能保证指定的 Groovy 代码字符串成功执行,不会导致任何异常。

3.1.2 shouldFail 方法

shouldFail 用于查看指定代码块是否失败,如果失败,断言成立,否则断言失败。

void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]
    shouldFail {
        numbers.get(4)
    }
}

上例使用了基本的 shouldFail 方法接口,使用了一个 groovy.lang.Closure 做单一参数。Closure 持有的代码被认为会在运行时中止。

如果我们要对特定 java.lang.Exception 类型进行 shouldFail 断言,我们可以使用一个 shouldFail 实现,其中第一个参数为 Exception 类,第二个参数为 Closure 类。

void testInvalidIndexAccess2() {
    def numbers = [1,2,3,4]
    shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
}

如果抛出 IndexOutOfBoundsException (或其后代类),则测试用例失败。

shouldFail 其实还有一个隐藏的优点:返回异常消息。如果想对异常错误消息进行断言,这一点很有用。

void testInvalidIndexAccess3() {
    def numbers = [1,2,3,4]
    def msg = shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
    assert msg.contains('Index: 4, Size: 4')
}
3.1.3 notYetImplemented 方法

notYetImplemented 方法受 HtmlUnit 影响很大。允许编写一个测试方法,但把它标记为还未实现。只要测试方法失败,并且被标记为 notYetImplemented,测试就依然能够通过。

void testNotYetImplemented1() {  
    if (notYetImplemented()) return     1⃣️

    assert 1 == 2                        2⃣️   
}

1⃣️ 要想使 GroovyTestCase 获取当前方法堆栈,就需要调用 notYetImplemented
2⃣️ 只要测试结果为 false,测试执行就会成功。

可以用 @NotYetImplemented 注释来替代 notYetImplemented 方法。它能注释一个未实现方法,带有和 GroovyTestCase##notYetImplemented 相同的行为,只是不需要调用 notYetImplemented 方法。

@NotYetImplemented
void testNotYetImplemented2() {
    assert 1 == 2
}

3.2 JUnit 4

利用 Groovy 编写 JUnit 4 测试用例没有任何限制。groovy.test.GroovyAssert 可以保存各种静态方法,它们可以用于替代 JUnit 4 测试中的 GroovyTestCase 方法。

import org.junit.Test

import static groovy.test.GroovyAssert.shouldFail

class JUnit4ExampleTests {

    @Test
    void indexOutOfBoundsAccess() {
        def numbers = [1,2,3,4]
        shouldFail {
            numbers.get(4)
        }
    }

}

如上所示,GroovyAssert 中的静态方法都在类定义之前导入进来,所以 shouldFail 可以像在 GroovyTestCase 中那样使用。

由于 groovy.test.GroovyAssert 来源自 org.junit.Assert,所以它继承了 JUnit 所有的断言方法。但由于强力断言语句的引入,依赖断言语句成了一种良好实践,不再需要 JUnit 断言方法,而改善的消息也成为了这样做的一个主要因素。

GroovyAssert.shouldFail 并不绝对等于 GroovyTestCase.shouldFailGroovyTestCase.shouldFail 返回异常消息,GroovyAssert.shouldFail 返回异常本身,获取异常消息还需要写一些代码,但反过来说也不是没有好处,你可以访问异常的其他属性和方法:

@Test
void shouldFailReturn() {
    def e = shouldFail {
        throw new RuntimeException('foo',
                                   new RuntimeException('bar'))
    }
    assert e instanceof RuntimeException
    assert e.message == 'foo'
    assert e.cause.message == 'bar'
}

4 利用 Spock 测试

Spock 是用于 Java 与 Groovy 程序的一种测试和规范框架。优雅以及高度表达规范的 DSL 都是它脱颖而出的重要因素。在实践中,Spock 规范会以 Groovy 类的形式呈现。虽然编写为 Groovy 类形式,但却可以用于测试 Java 类。Spock 可用于进行单元测试、集成测试,以及 BDD 测试(行为驱动的开发),它并不局限于某一特定类别的测试框架或库。

除了这些非常惊艳的功能外,对于如何在第三方库中利用先进的 Groovy 编程语言特性(比如使用 Groovy AST 转换)这种问题,Spock 也堪称最佳答案。

这一部分内容并不详细介绍 Spock 的具体使用细节,只概述了 Spock 的一些基本内容以及它在各种测试中的应用情况(单元测试、集成测试、功能测试以及其他类型的测试)。

下面首先剖析 Spock 规范,它能让你迅速了解 Spock 的功能。

4.1 规范

通过 Spock,我们可以编写描述相关系统功能(属性、方面)的规范。这里所说的“系统”可以是任何东西,既可以是单个类,也可以是整个的应用程序,更准确的描述应该是“遵循某种规范的系统”。“功能描述”始于系统与其协作对象的一个特殊快照,这种快照被称为“功能夹具”。

Spock 规范类衍生自 spock.lang.Specification。具体的规范类可能含有字段、夹具方法、功能方法以及辅助方法。

下面来看看一个假想的 Stack 类的简单规范,它只带有一个功能方法:

class StackSpec extends Specification {

    def "adding an element leads to size increase"() {    1⃣️
        setup: "a new stack instance is created"        2⃣️
            def stack = new Stack()

        when:                                           3⃣️
            stack.push 42

        then:                                           4⃣️
            stack.size() == 1
    }
}

1⃣️ 功能方法,按照归约,以字符串字面量形式命名。
2⃣️ 设置块,包含用于功能所需完成工作的所有设置内容。
3⃣️ When 语句块描述了一种刺激性条件,由功能规范所确定的一种目标特定行为。 4⃣️ Then 语句块包含的表达式能够验证由 When 语句块触发的代码结果。

Spock 功能规范被定义为 spock.lang.Specification 类中的方法。它们使用字符串字面量而不是方法名来描述功能。

功能方法持有多个语句块,在我们这个小例子中,使用了 setupwhenthensetup 很特殊,它是可选的,可以用来配置出现在功能方法中的本地变量。when 语句块定义了刺激条件,是描述该刺激条件响应的 then 语句块的对应物。

注意,StackSpec 中的 setup 方法额外还存在一个描述字符串,这种描述字符串是可选的,可以添加到任何语句块标签(setupwhenthen)的后面。

4.2 更多 Spock 的详细信息

Spock 还提供更多的高级功能,比如像数据表及高级模拟功能,等等。可参看 Spock GitHub page获取更多信息及下载信息。

5 利用 Geb 进行功能测试

Geb 是一种功能性的 Web 测试及抓取库,可以完美地与 JUnit 和 Spock 集成。它基于 Selenium Web 驱动,像 Spock 一样,它也能提供 Groovy DSL 来编写 Web 应用的功能测试。

以下这些功能使其成为一种非常好的功能测试库:

  • 通过 jQuery 语句(比如 $ 函数)进行 DOM 访问;
  • 实现页面模式;
  • 支持特定 Web 组件(比如菜单栏)的模块化
  • 通过 JS 变量与 JavaScript 相集成。

这一部分内容并不详细介绍 Geb 的具体使用细节,只概述了 Geb 的一些基本内容以及它在功能测试中的应用情况。

接下来就通过范例来介绍针对带有一个搜索字段的 Web 页面,如何利用 Geb 编写功能测试。

5.1 Geb 脚本

虽然 Geb 可以单独用于 Groovy 脚本,但在很多场合中,它是和其他测试框架联合使用的。Geb 自带很多可以用于 JUnit 3/4、TestNG 以及 Spock 中的基本类。这些基本类都是 Geb 额外模块的一部分,需要以依赖的形式添加进来。

比如说,在下例中,在 JUnit 4 测试中,必须使用 @Grab 依赖来运行带有 Selenium Firefox 驱动的 Geb。需要 JUnit 3/4 支持的模块是 geb-junit

@Grapes([
    @Grab("org.gebish:geb-core:0.9.2"),
    @Grab("org.gebish:geb-junit:0.9.2"),
    @Grab("org.seleniumhq.selenium:selenium-firefox-driver:2.26.0"),
    @Grab("org.seleniumhq.selenium:selenium-support:2.26.0")
])

Geb 的中心类是 geb.Browser 类,正如其名称所暗示的那样,用来浏览页面及访问 DOM 元素:

def browser = new Browser(driver: new FirefoxDriver(), baseUrl: 'http://myhost:8080/myapp')                                  1⃣️ 
browser.drive {
    go "/login"                               2⃣️

    $("##username").text = 'John'               3⃣️
    $("##password").text = 'Doe'

    $("##loginButton").click()

    assert title == "My Application - Dashboard"

1⃣️ 创建了一个新的 Browser 实例。在本例中,它使用了 Selenium FirefoxDriver,并设置了 baseUrl
2⃣️ go 用于导航至一个 URL 或相对 URL。
3⃣️ $ 和 CSS 选择器一起用于访问 DOM 字段的 usernamepassword

Browser 类带有的 drive 方法会将所有方法或属性的调用都委托给当前的 browser 实例。Browser 配置不能在行内完成,它可以设置在外部的 GebConfig.groovy 配置文件中。在实际中,Browser 类大多由 Geb 测试基本类所隐藏。它们会将所有丢失的属性及方法调用都委托给存在于后台中的当前 browser 实例。

class SearchTests extends geb.junit4.GebTest {

    @Test
    void executeSeach() {
        go 'http://somehost/mayapp/search'        1⃣️          
        $('##searchField').text = 'John Doe'         2⃣️    
        $('##searchButton').click()                    3⃣️  

        assert $('.searchResult a').first().text() == 'Mr. John Doe'          4⃣️
    }
}

1⃣️ Browser##go 获取相对或绝对链接,调用页面。
2⃣️ Browser##$ 用于访问 DOM 内容。可以使用由潜在的 Selenium 驱动所支持的任何 CSS 选择器。
3⃣️ click 用于点击按钮。
4⃣️ $ 用于获取 searchResult 块的首个链接。

上例展示了一个利用 JUnit 4 基本类 geb.junit4.GebTest 的简单 Geb Web 测试。注意在该例中,Browser 配置是放在外部的。GebTest 会将一些方法(go$)都委托给潜在的 browser 实例。

5.2 更多 Geb 相关内容

在之前的内容中,只介绍了一些 Geb 功能的皮毛,更多详情参见其项目主页