在我的另一篇文章中,结合实践经验,总结了当前阶段企业级开发的一套基于groovy的实用测试环境,本文将进一步说明使用groovy做测试的原因,并详细描述groovy语言支持编写java测试代码的诸多优秀特性,以及使用groovy编写测试过程中数量微小但不得不时刻保持谨慎的“坑”。
不夸张的说,掌握了本文所述的常见“测试”桥段,人人都能成为写测试的高手。但是要完全弄懂,还是需要多看几本groovy书(比如《groovy编程》),或者访问groovy官网里的文档。
主要有以下几个原因:
得益于groovy的类型推断特性,以及可选类型(Optional Type)特性,类型将不再像java中那样复杂。
比如,groovy支持更加简单的对象构造语法:
def str = 'a simple string'
assert str.class = String // 不用写成String.class
// 打印利器:GString,再也不用拼接字符串了。
// ${expr},注意GString里是可以使用表达式的
def greeting = 'hello'
def gstr = "GString: ${greeting}"
assert gstr instanceof GString
assert gstr == 'GString: hello' // ==调用了equals,而不是判断引用相等
def num = 1
assert num instanceof int
assert num.class == Integer
又如,groovy对java bean的支持:
@Data // lombok生成了equals、toString等方法
class Person {
String name
Integer age
}
def p1 = new Person(name:'hx', age: 18)
// 这里p1.name实际是调用了getName方法,并没有破坏封装的原则。
println "person name: ${p1.name}, age: ${p1.age}"
// 造数据利器:map转java bean
def p2 = [name:'hx', age:18] as Person
// 判等利器:注意这里利用了lombok生成的equals方法
assert p1 == p2
def p3 = p1
assert p3.is(p1) // 判断引用相等
还有不得不说的,groovy闭包的使用:
// 闭包利器:groovy闭包转换为接口实现或java8的lambda表达式
interface MyService {
void doWork()
}
def myService = {
// do your work
} as MyService
这里需要进行说明一个问题:groovy提供的动态特性真的必要么?比如,给类、对象加新方法、字段之类的特性,一般是很少用到的,始终使用强类型支持,可以最大程度地利用静态语言的优势——编译器检查。但在某些地方,稍微使用动态语言的特性,能够简化代码,提升可读性。
比如下面的例子给对象添加了下标访问的快捷方法:
@Autowired
ApplicationContext context // 注入spring上下文
// 通过闭包,为对象动态实现groovy的下标访问协议方法
context.metaClass.getAt = {
if(it instanceof Class) {
// 调用java方法时,需要显式类型以提示编译器静态绑定的方法
// 这是调用java重载方法需要注意的
return context.getBean(it as Class)
}
return context.getBean(it as String)
}
// 通过名称获取bean
def service = context['myService']
// 或者通过类型获取bean
def anotherService = context[AnotherService]
当使用idea编写groovy测试时,是否遵循尽量强类型的约定,一个重要的指标就是,代码不能出现编译器警告(编辑器右上角是否有绿色勾勾)。
一般而言,类的域要显式声明其类型,局部变量可以省略类型,使用编译器的类型推断,当调用java方法并且编译器无法推断的时候,最好加上as给编译器一些提示。
这样做的好处是:代码是类型安全的,as转换失败的时候,也会fail fast。毕竟即使写python,动态拓展等特性也并不总是那么有用。
面对数量巨大的第三方jar包,没有哪个开发者能够抵挡住其诱惑。这里唯一需要注意的是上面提到过的类型方面的问题,java方法是需要明确指明类型的,处理好这点,就可以放心地在groovy中,使用现有的海量第三方jar包。
比如,在groovy代码中声明spring bean:
@Configuration
class SomeConfig {
// 这里SomeClass写成def,返回值的静态类型就成了Object
// spring在bean注入时就会提示找不到该类型的bean
@Bean
SomeClass someObject() {}
}
前面已经提到过一些特性,回忆一下,包括:
当然,还有更多给力的东西。这里将不纠结于具体的语法,只说明特性中支持我们方便地编写测试的部分。
熟悉python的人都知道其强大的列表字面量及列表解析特性。作为groovy的灵感来源之一,groovy不会错过这些。结合闭包,groovy甚至比python做得更加优秀。
def nums = [1, 2, 3]
assert nums instanceof List
// 转set
def set = nums as Set
assert set instanceof LinkedHashSet
// 迭代
nums.each { println it }
// map
assert nums.collect{ it + 1 }== [2, 3, 4]
// flatten
assert [[1], [2, 3]].flatten() == [1, 2, 3]
// filter
assert nums.findAll{ it > 1 } == [2, 3]
// range
assert (0..2).collect{ it + 1 } == [1, 2, 3]
// any
assert nums.any{ it == 3 }
// every
assert nums.every{ it > 0 }
groovy map的key默认就是string类型,所以使用时可以省略字符串的引号。
def m = [a:1, b:2]
// 使用其他类型的key
def c = 'c'
def d = 'd'
def mm = [(c):3, (d):4]
// 其他操作和列表类似
assert m.findAll { it.key == 'a' } == [a:1]
前文中大量使用了assert关键字,这无疑又是groovy的一大利器。相比于java的assert,groovy的assert是默认开启的,并且,当assert断言失败时,groovy会给出更加详细的信息。如图所示。
在spock测试框架的then块中,使用的也是groovy的powerassert断言特性。
提到assert,需要注意groovy对布尔值的隐式转换。在需要boolean的语境下,groovy会将空列表、空映射、空字符串、0、0.0、null都转换为false。这简化了if语句的编写。
assert true
assert !''
assert ![]
assert ![:]
assert !0
assert 1
assert !null
if(0) assert false
在groovy3中,计算字符串的md5变得十分容易:直接调用'str'.md5()
即可。
此外,在groovy中,可以将函数的最后一个闭包写到代码块中:
def fun(Clousre c) {...}
// 调用
fun { println 'in closure' }
又如groovy提供的sql支持:
def sql = new Sql(dataSource)
sql.rows('select name from person') { it.name } == ['hx']
还有groovy对xml、json解析的支持、对swing应用编写的简化等,都让人眼前一亮。
最后用一个集成测试的例子来结束我们的话题。
@Service
class MyService {
Person findPersonInRedis(String name) {
// 省略
}
}
class MyServiceTest extends IntegrationTest{
@Autowired
MyService myService
@Autowired
JsonRedisTemplate jsonRedisTemplate
void 'test find the person saved in redis'() {
def person = [name:'hx', age:18] as Person
assert person.name == 'hx'
// 注意这里的类型转换,将GString转为java的String
def redisKey = "person:${person.name}" as String
assert redisKey == 'person:hx'
jsonRedisTemplate.opsForValue().set(redisKey, person)
assert jsonRedisTemplate.hasKey(redisKey)
def foundPerson = myService.findPersonInRedis(person.name)
assert person == foundPerson
jsonRedisTemplate.delete('person:hx')
}
}
可以看到,测试的工作得到了简化,代码变得更加短小和清晰,当然,更多精彩的内容还需要读者亲自去实践。
关于单元测试,推荐最好使用基于groovy的spock成都市框架,这样可以将mock工作得到最大程度的简化。这个话题可以参考我的另外一篇介绍spock测试框架的博客。