测试指南

1. 简介

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

本章将从特定于语言的测试功能开始,然后更详细地介绍 JUnit 集成、Spock 用于规范以及 Geb 用于功能测试。最后,我们将概述其他已知与 Groovy 协同工作的测试库。

2. 语言特性

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

2.1. 强力断言

编写测试意味着使用断言来表达假设。在 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 本节显示 std-err 输出

每当断言无法成功验证时抛出的 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 子类一起使用时处理自定义 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 类支持通过允许定义协作者行为的松散排序预期来进行类的隔离测试(通常为单元测试)。典型的测试场景包括一个待测试类和一个或多个协作者。在这种情况下,通常需要仅测试待测试类的业务逻辑。一种实现此目的的策略是使用简化的桩对象来替换协作者实例,以帮助隔离测试目标中的逻辑。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. Expando 元类 (EMC)

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

每个 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.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.Iterable上添加的eachCombination方法可用于将函数(在本例中为groovy.lang.Closure)应用于combinations方法构建的每个组合。

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

各种框架和构建工具都集成了 Cobertura。对于 Grails,有一个基于 Cobertura 的 代码覆盖率插件,对于 Gradle,有一个 gradle-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,但这并不意味着您不能在项目中使用 JUnit 4 功能。事实上,最新的 Groovy 版本包含一个捆绑的 JUnit 4,它附带一个向后兼容的TestCase实现。在 Groovy 邮件列表中曾有一些关于是否使用GroovyTestCase或 JUnit 4 的讨论,结果表明这主要是一个喜好问题,但使用GroovyTestCase,您可以免费获得大量方法,这些方法使某些类型的测试更容易编写。

在本节中,我们将了解GroovyTestCase提供的一些方法。可以在 groovy.test.GroovyTestCase 的 JavaDoc 文档中找到这些方法的完整列表,不要忘记它继承自junit.framework.TestCasejunit.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 断言方法。但是,随着 Power 断言语句的引入,事实证明最好依赖于断言语句,而不是使用 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

在使用 JUnit5 时,JUnit4 下描述的大多数方法和辅助类都适用,但是 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类中的方法。它们使用字符串文字而不是方法名来描述功能。

一个功能方法包含多个块,在我们的示例中,我们使用了 `setup`、`when` 和 `then`。`setup` 块是特殊的,因为它是可选的,并允许配置在功能方法内部可见的局部变量。`when` 块定义了刺激,是 `then` 块的伴侣,`then` 块描述了对刺激的响应。

请注意,上面的 `StackSpec` 中的 `setup` 方法还包含一个描述字符串。描述字符串是可选的,可以在块标签(如 `setup`、`when`、`then`)之后添加。

4.2. 更多 Spock

Spock 提供了更多功能,例如数据表或高级模拟功能。请随时参考 Spock GitHub 页面 获取更多文档和下载信息。

5. Geb 功能测试

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

Geb 具有很多功能,使其成为功能测试库的理想选择

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

  • 实现页面模式

  • 支持使用模块对某些 Web 组件(例如菜单栏等)进行模块化

  • 通过 JS 变量集成 JavaScript

本节不应作为 Geb 使用的详细指南,它应该更侧重于让您了解 Geb 是什么,以及如何利用它进行功能测试。

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

5.1. 一个 Geb 脚本

虽然 Geb 可以单独在 Groovy 脚本中使用,但在许多情况下,它与其他测试框架结合使用。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 选择器一起用于访问 `username` 和 `password` DOM 字段。

`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` 块中获取第一个链接

上面的示例展示了一个简单的 Geb Web 测试,使用 JUnit 4 基类 `geb.junit4.GebTest`。请注意,在这种情况下,`Browser` 配置是外部化的。`GebTest` 将 `go` 和 `$` 等方法委托给底层的 `browser` 实例。

5.2. 更多 Geb

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