1. 介绍

Groovy 编程语言对编写测试提供了极大的支持。除了语言特性以及与最先进的测试库和框架的测试集成外,Groovy 生态系统还诞生了一套丰富的测试库和框架。

本章将从语言特定的测试特性开始,然后深入研究 JUnit 集成、用于规范的 Spock 和用于功能测试的 Geb。最后,我们将概述其他已知与 Groovy 协同工作的测试库。

2. 语言特性

除了对 JUnit 的集成支持外,Groovy 编程语言还带有一些已被证明对测试驱动开发非常有价值的特性。本节将深入探讨它们。

2.1. 强力断言

编写测试意味着使用断言来 формулировать assumptions。在 Java 中,这可以通过使用 J2SE 1.4 中添加的 assert 关键字来完成。在 Java 中,assert 语句可以通过 JVM 参数 -ea(或 -enableassertions)和 -da(或 -disableassertions)启用。Java 中的断言语句默认禁用。

Groovy 带有一个相当强大的 assert 变体,也称为强力断言语句。Groovy 的强力 assert 与 Java 版本的不同之处在于,当布尔表达式验证为 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 中创建自定义模拟要容易得多。您通常可以使用简单的映射或闭包来构建自定义模拟。

以下各节将展示仅使用 Groovy 语言特性创建模拟和存根的方法。

2.2.1. 映射强制转换

通过使用映射或扩展,我们可以非常容易地实现协作对象的期望行为,如下所示:

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 强制转换的特性。这意味着在运行时可以推断目标 SAM 类型的情况下,as 运算符不是必需的。这种强制转换可能在测试中用于模拟整个 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 类支持(通常是单元)隔离测试类,通过允许定义协作对象的松散有序行为期望。典型的测试场景涉及一个被测类和一个或多个协作对象。在这种场景中,通常只测试 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 不能用于测试静态编译的类,例如 Java 类或使用 @CompileStatic 的 Groovy 类。要存根和/或模拟这些类,您可以使用 Spock 或 Java 模拟库之一。

2.2.4. 扩展元类(EMC)

Groovy 包含一个特殊的 MetaClass,即所谓的 ExpandoMetaClass (EMC)。它允许使用巧妙的闭包语法动态添加方法、构造函数、属性和静态方法。

每个 java.lang.Class 都提供一个特殊的 metaClass 属性,它将提供一个 ExpandoMetaClass 实例的引用。扩展元类不限于自定义类,它也可以用于 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'
模拟构造函数可能看起来像一个不值得考虑的 hack,但即使如此,也可能有有效的用例。一个例子可以在 Grails 中找到,其中域类构造函数在运行时借助 ExpandoMetaClass 添加。这使得域对象可以在 Spring 应用程序上下文中注册自己,并允许注入服务或由依赖注入容器控制的其他 bean。

如果您想在每个测试方法级别更改 metaClass 属性,则需要删除对元类所做的更改,否则这些更改将在测试方法调用之间持续存在。通过替换 GroovyMetaClassRegistry 中的元类来删除更改。

GroovySystem.metaClassRegistry.removeMetaClass(String)

另一种选择是注册一个 MetaClassRegistryChangeEventListener,跟踪已更改的类并在您选择的测试运行时的清理方法中删除更改。一个很好的例子可以在 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.IterableeachCombination 方法可以用于对由 combinations 方法构建的每个组合应用一个函数(或在此情况下为 groovy.lang.Closure)。

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

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

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

2.4. 工具支持

2.4.1. 测试代码覆盖率

代码覆盖率是衡量(单元)测试有效性的有用指标。代码覆盖率高的程序比没有或覆盖率低的程序发生严重错误的几率更低。为了获取代码覆盖率指标,通常需要在执行测试之前对生成的字节码进行插桩。支持 Groovy 的工具之一是 Cobertura

以下代码清单展示了如何在 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 进行测试

Groovy 以以下方式简化了 JUnit 测试

  • 您使用与 Java 测试相同的总体实践,但您可以在测试中采用 Groovy 简洁的语法,使它们更加简洁。如果您愿意,甚至可以使用编写测试领域特定语言(DSL)的功能。

  • 有许多辅助类简化了许多测试活动。具体细节在某些情况下因您使用的 JUnit 版本而异。我们稍后将介绍这些细节。

  • Groovy 的 PowerAssert 机制在您的测试中非常好用。

  • Groovy 认为测试非常重要,您应该能够像运行脚本或类一样轻松地运行它们。这就是为什么 Groovy 在使用 groovy 命令或 GroovyConsole 时包含一个自动测试运行器。这为您提供了除了运行测试之外的一些附加选项。

在以下章节中,我们将仔细研究 JUnit 3、4 和 5 的 Groovy 集成。

3.1. JUnit 3

也许支持 JUnit 3 测试最著名的 Groovy 类之一是 GroovyTestCase 类。它派生自 junit.framework.TestCase,提供了一系列附加方法,使 Groovy 中的测试变得轻而易举。

尽管 GroovyTestCase 继承自 TestCase,但这并不意味着您不能在项目中D使用 JUnit 4 特性。事实上,最新版本的 Groovy 捆绑了 JUnit 4,并且附带了向后兼容的 TestCase 实现。在 Groovy 邮件列表中就使用 GroovyTestCase 还是 JUnit 4 进行了一些讨论,结果是这主要取决于个人喜好,但使用 GroovyTestCase 您可以免费获得许多方法,使某些类型的测试更容易编写。

在本节中,我们将介绍 GroovyTestCase 提供的一些方法。可以在 groovy.test.GroovyTestCase 的 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 断言方法调用表达式的可读性更高。

GroovyTestCase 添加了一个有趣的断言方法:assertScript。它确保给定的 Groovy 代码字符串成功执行而没有抛出任何异常。

void testScriptAssertions() {
    assertScript '''
        def x = 1
        def y = 2

        assert x + y == 3
    '''
}

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,我们可以通过使用将 Exception 类作为第一个参数,Closure 作为第二个参数的 shouldFail 实现来完成。

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') ||
        msg.contains('Index 4 out-of-bounds for length 4') ||
        msg.contains('Index 4 out of bounds for length 4')
}

3.1.3. notYetImplemented 方法

notYetImplemented 方法受到 HtmlUnit 的极大影响。它允许编写测试方法但将其标记为尚未实现。只要测试方法失败并标记为 notYetImplemented,测试就会通过。

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

    assert 1 == 2                     (2)
}
1 调用 notYetImplemented 对于 GroovyTestCase 获取当前方法栈是必需的。
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 断言方法。然而,随着强力断言语句的引入,依靠断言语句被证明是一种好的实践,其中改进的消息是主要原因。

值得一提的是,GroovyAssert.shouldFailGroovyTestCase.shouldFail 并不完全相同。GroovyTestCase.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'
}

3.3. JUnit 5

JUnit4 中描述的大多数方法和辅助类在 JUnit5 中也适用,但 JUnit5 在编写测试时使用了一些略有不同的类注解。有关更多详细信息,请参阅 JUnit5 文档。

根据正常的 JUnit5 指南创建您的测试类,如本例所示

class MyTest {
  @Test
  void streamSum() {
    assertTrue(Stream.of(1, 2, 3)
      .mapToInt(i -> i)
      .sum() > 5, () -> "Sum should be greater than 5")
  }

  @RepeatedTest(value=2, name = "{displayName} {currentRepetition}/{totalRepetitions}")
  void streamSumRepeated() {
    assert Stream.of(1, 2, 3).mapToInt(i -> i).sum() == 6
  }

  private boolean isPalindrome(s) { s == s.reverse()  }

  @ParameterizedTest                                                              (1)
  @ValueSource(strings = [ "racecar", "radar", "able was I ere I saw elba" ])
  void palindromes(String candidate) {
    assert isPalindrome(candidate)
  }

  @TestFactory
  def dynamicTestCollection() {[
    dynamicTest("Add test") { -> assert 1 + 1 == 2 },
    dynamicTest("Multiply Test", () -> { assert 2 * 3 == 6 })
  ]}
}
1 如果您的项目中尚未包含此依赖项,则此测试需要额外的 org.junit.jupiter:junit-jupiter-params 依赖项。

如果您的 IDE 或构建工具支持并配置了 JUnit5,您可以运行测试。如果您在 GroovyConsole 或通过 groovy 命令运行上述测试,您将看到运行测试结果的简短文本摘要。

JUnit5 launcher: passed=8, failed=0, skipped=0, time=246ms

更详细的信息可在 FINE 日志级别获得。您可以配置您的日志以显示此类信息,或按如下方式以编程方式进行:

@BeforeAll
static void init() {
  def logger = Logger.getLogger(LoggingListener.name)
  logger.level = Level.FINE
  logger.addHandler(new ConsoleHandler(level: Level.FINE))
}

4. 使用 Spock 进行测试

Spock 是一个用于 Java 和 Groovy 应用程序的测试和规范框架。它之所以脱颖而出,是因为其优美且高度富有表现力的规范 DSL。在实践中,Spock 规范是作为 Groovy 类编写的。尽管用 Groovy 编写,但它们可以用于测试 Java 类。Spock 可用于单元测试、集成测试或 BDD(行为驱动开发)测试,它不将自己归入特定类别的测试框架或库。

除了这些出色的功能外,Spock 还是如何利用第三方库中的高级 Groovy 编程语言功能的一个很好的例子,例如,通过使用 Groovy AST 转换。
本节不应作为 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 页面以获取更多文档和下载信息。

5. 使用 Geb 进行功能测试

Geb 是一个功能性 Web 测试和抓取库,它与 JUnit 和 Spock 集成。它基于 Selenium Web 驱动程序,并且像 Spock 一样,提供了 Groovy DSL 来编写 Web 应用程序的功能测试。

Geb 具有出色的功能,使其非常适合作为功能测试库:

  • 通过类似 JQuery 的 $ 函数访问 DOM

  • 实现页面模式

  • 通过模块支持某些 Web 组件(例如菜单栏等)的模块化

  • 通过 JS 变量与 JavaScript 集成

本节不应作为使用 Geb 的详细指南,它应该更多地给人留下 Geb 是什么以及如何将其用于功能测试的印象。

下一节将举例说明如何使用 Geb 为一个带有一个搜索字段的简单网页编写功能测试。

5.1. 一个 Geb 脚本

尽管 Geb 可以独立用于 Groovy 脚本中,但在许多场景中,它与 L其他测试框架结合使用。Geb 附带了各种基本类,可以在 JUnit 3、4、TestNG 或 Spock 测试中使用。基本类是需要作为依赖项添加的额外 Geb 模块的一部分。

例如,以下 @Grab 依赖项可用于在 JUnit4 测试中使用 Selenium Firefox 驱动程序运行 Geb。JUnit 3/4 支持所需的模块是 geb-junit4

@Grab('org.gebish:geb-core:0.9.2')
@Grab('org.gebish:geb-junit4: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 元素。

import geb.Browser
import org.openqa.selenium.firefox.FirefoxDriver

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 或相对 URI
3 $ 和 CSS 选择器一起用于访问 usernamepassword DOM 字段。

Browser 类带有一个 drive 方法,该方法将所有方法/属性调用委托给当前的 browser 实例。Browser 配置不必内联完成,它也可以外部化,例如在一个 GebConfig.groovy 配置文件中。实际上,Browser 类的使用大多被 Geb 测试基类隐藏。它们将所有缺失的属性和方法调用委托给后台存在的当前 browser 实例。

class SearchTests extends geb.junit4.GebTest {

    @Test
    void executeSearch() {
        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 配置是外部化的。GebTestgo$ 等方法委托给底层的 browser 实例。

5.2. 更多 Geb

在上一节中,我们只触及了 Geb 可用功能的一小部分。有关 Geb 的更多信息,请访问 项目主页