使用groovy做java测试

1、背景

在我的另一篇文章中,结合实践经验,总结了当前阶段企业级开发的一套基于groovy的实用测试环境,本文将进一步说明使用groovy做测试的原因,并详细描述groovy语言支持编写java测试代码的诸多优秀特性,以及使用groovy编写测试过程中数量微小但不得不时刻保持谨慎的“坑”。

不夸张的说,掌握了本文所述的常见“测试”桥段,人人都能成为写测试的高手。但是要完全弄懂,还是需要多看几本groovy书(比如《groovy编程》),或者访问groovy官网里的文档。

2、why groovy

主要有以下几个原因:

  • 在java13普及前,甚至普及后较长的一段时期内,groovy语法依旧更加简单
  • groovy兼容java,二者使用相同的类型系统,兼容性在诸多jvm语言中绝无仅有
  • groovy能够使用java的海量第三方库,这个非常关键,很多工具、框架,如lombok、springboot等,都是即插即用
  • groovy具有一些优秀的特性,能减少编写测试的痛楚
  • groovy提供了更好的API,可以帮助减少代码噪声(less verbose)

简洁的语法

得益于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,动态拓展等特性也并不总是那么有用。

groovy使用第三方java库

面对数量巨大的第三方jar包,没有哪个开发者能够抵挡住其诱惑。这里唯一需要注意的是上面提到过的类型方面的问题,java方法是需要明确指明类型的,处理好这点,就可以放心地在groovy中,使用现有的海量第三方jar包。

比如,在groovy代码中声明spring bean:

@Configuration
class SomeConfig {
  // 这里SomeClass写成def,返回值的静态类型就成了Object
  // spring在bean注入时就会提示找不到该类型的bean
  @Bean
  SomeClass someObject() {}
}

3、groovy支持编写测试的特性

前面已经提到过一些特性,回忆一下,包括:

  • 使用强大的GString让打印信息更加简便
  • 将map转换为java bean制造测试数据
  • 闭包转换为lambda表达式、匿名接口实现类等

当然,还有更多给力的东西。这里将不纠结于具体的语法,只说明特性中支持我们方便地编写测试的部分。

列表支持

熟悉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]

PowerAssert

前文中大量使用了assert关键字,这无疑又是groovy的一大利器。相比于java的assert,groovy的assert是默认开启的,并且,当assert断言失败时,groovy会给出更加详细的信息。如图所示。
使用groovy做java测试_第1张图片
在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

更便捷的API

在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应用编写的简化等,都让人眼前一亮。

4、一个例子

最后用一个集成测试的例子来结束我们的话题。

@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测试框架的博客。

你可能感兴趣的:(groovy应用)