测试指南
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 | 本节显示标准错误输出 |
每当断言无法成功验证时抛出的 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 中创建自定义模拟要容易得多。您通常可以使用简单的 Map 或闭包来构建自定义模拟。
以下部分展示了仅使用 Groovy 语言特性创建模拟和存根的方法。
2.2.1. Map 强制转换
通过使用 map 或 expandos,我们可以非常容易地整合协作者的所需行为,如下所示:
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
运算符可用于将 Map 强制转换为特定类。给定的 Map 键被解释为方法名,值(即 groovy.lang.Closure
块)被解释为方法代码块。
请注意,如果您处理自定义的 java.util.Map 子类与 as 运算符结合使用,Map 强制转换可能会遇到问题。Map 强制转换机制直接针对某些集合类,它不考虑自定义类。 |
2.2.2. 闭包强制转换
'as' 操作符可以与闭包以一种巧妙的方式一起使用,这对于简单场景下的开发者测试非常有用。我们尚未发现此技术强大到可以取代动态模拟,但在简单情况下它仍然非常有用。
持有单个方法(包括 SAM(Single Abstract Method)类)的类或接口可以用于将闭包块强制转换为给定类型的对象。请注意,为了实现这一点,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 的调用(可选)检查方法调用的数量是否符合预期。 |
MockFor
和 StubFor
不能用于测试静态编译类,例如 Java 类或使用 @CompileStatic
的 Groovy 类。要存根和/或模拟这些类,您可以使用 Spock 或其中一个 Java 模拟库。
2.2.4. Expando Meta-Class (EMC)
Groovy 包含一个特殊的 MetaClass
,即所谓的 ExpandoMetaClass
(EMC)。它允许使用简洁的闭包语法动态添加方法、构造函数、属性和静态方法。
每个 java.lang.Class
都提供一个特殊的 metaClass
属性,它将提供一个 ExpandoMetaClass
实例的引用。Expando Meta-Class 不仅限于自定义类,它也可以用于 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.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。
以下代码清单展示了如何在 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.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 | 对于 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.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 集成。它基于 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 块中获取第一个链接。 |
上面的示例展示了一个使用 JUnit 4 基类 geb.junit4.GebTest
的简单 Geb Web 测试。请注意,在这种情况下,Browser
配置是外部化的。GebTest
将 go
和 $
等方法委托给底层的 browser
实例。
5.2. 更多 Geb
在上一节中,我们只触及了 Geb 可用功能的皮毛。有关 Geb 的更多信息,请访问其项目主页。