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
(可选的)检查是否调用数目符合预期。
MockFor
和 StubFor
无法应用于静态编译类,比如使用 @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 代码覆盖报告和测试代码覆盖报告可以添加到持续集成构建任务中,可以为这些报告选择一些输出格式。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论