随着Google Android 团队宣布了Kotlin
成为官方支持语言,互联网上一片“Java已死”的报道又一次汹涌而来。然而每次热潮一过,Java
还是活得好好的,甚至我们将迎来半年一次更新的计划。事实上,Android团队并没有说用Kotlin
替换Java
,只是给大家多了一种选择。多一种选择意味着可以根据场景选择更合适的解决方案,而在一个项目中甚至可以共存多种解决方案。例如一个典型的Java Web项目,可能会有一些Shell
或Python
脚本来帮助构建或搭建本地开发环境。
root
|__src
|__main
|__java
|__scripts
|__seeder.py // 填充种子数据的Python脚本
填充种子数据的Python脚本,可能每个Java项目中都或多或少有其他语言的代码
之前有幸参加了一个Digital Marketing的项目,为某个国际知名运动品牌搭建微信服务号后台。在这个细分行业中,应用的生命周期往往很短,一个支撑市场活动的应用可能需要在2周内开发出来,但只上线使用1天。PHP
是该行业最流行的语言,但该品牌要求这次项目必须使用Java
开发(顺便提一句,Google对Digital Marketing项目的语言要求虽然没有那么苛刻,但是PHP
是被排除的,希望这两个案例不会导致歪楼)。考虑到该应用有较长的生命周期,自动化测试还是有必要的。既然生产代码本身必须是Java
,是否可以用更灵活的语言来写测试呢?于是我想到了Groovy
。
我对Groovy
并不熟悉,但由于经常使用gradle
这个构建工具,所以对它的语言特性略知一二。例如字符串操作在Groovy
中有一些有趣的增强。在Java
测试代码中很麻烦的json
字符串,可以很优雅的解决:
@Test
public void it_should_return_an_event() {
stubFor(get(urlEqualTo("/v1/events/12345"))
.willReturn(aResponse()
.withStatus(OK.value())
.withBody("" +
"{" +
"\"body"\:{" +
"\"id\": 12345," +
"\"registrationOpenDate\": \"2017-07-28T09:00:00\"," +
"\"capacity"\": 20," +
"\"currentParticipation"\": 12," +
"}" +
"}" +
"))); // 繁琐的转义符导致一段简单的json字符串被分隔的面目全非
Event availability = subject.extractEvent("12345")
assertThat(availability.eventId, is("12345"));
// omitted asserts
}
在Java代码中拼接字符串很繁琐,而且可读性不好,往往只能将json字符串抽取到单独的文件中,但抽取到文件中一方面降低了测试的运行速度,另一方面在调试时需要在测试代码和json文件两个窗口中切换,体验不是很流畅
@Test
void "it should return an event"() {
stubFor(get(urlEqualTo("/v1/events/12345"))
.willReturn(aResponse()
.withStatus(OK.value())
.withBody("""
{
"body":{
"id": 12345,
"registrationOpenDate": "2017-07-28T09:00:00",
"capacity": 20,
"currentParticipation": 12,
}
}
""")))
def availability = subject.extractEvent("12345")
assert availability.eventId == "12345"
// omitted asserts
}
通过”"”可以保留json字符串的格式,并且免去了讨厌的转义符
另外一个关于Groovy
字符串的小应用是方法名,当我们在Java
测试方法名上尝试驼峰或是下划线分割时,Groovy
可以直接用字符串定义方法名
@Test
public void itShouldEmitOrderCanceledEvent_whenCancelOrder_givenOrderIsConfirmed() {
// omitted code
}
常见的Java测试方法命名规则:下划线分隔given, when, then,分隔分割子句中的单词
@Test void "it should emit OrderCanceledEvent when I cancel the order given the order is confirmed"() { // omitted code }
Groovy可以将字符串作为方法名,更容易使用自然语言来描述测试的意图
Java的强类型检查是工程上的利器,但有时在测试中就显得有些耿直了
@Test
public void testPricingPolicy() {
// omitted code
assertThat(price.getQuantity(), is(2L)); // fails given 2
}
在Java中,如果quantity是Long型,在测试里我们也必须精确描述,但是在数字后加个L很容易导致看错
@Test void "test pricing policy"() { // omitted code assert price.quantity == 2 }
在Java中,如果quantity是Long型,在测试里我们也必须精确描述,但是在数字后加个L很容易导致看错
Groovy还提供了一些简洁的类型声明特性,可以帮助简化Map, List的构造。在测试代码中这些片段往往是为了构造测试数据,其实我们更关注的是数据,甚至是测试数据的差异,希望尽可能的去除语法噪声
@Test
public void testListBuilder() {
// omitted code
List<String> names = new ArrayList<>();
names.add("john");
names.add("ben");
names.add("marry");
}
在Java中声明并实例化一个List——啰嗦
@Test void "test list builder"() { // omitted code def names = ["john", "ben" "marry"] }
在Groovy中,[] 可以用来实例化List,写起来还挺顺手的
以上这些只是语法糖让Groovy
看起来好象是Ruby on Java
,它几乎继承了ruby
所有的优秀语法,同样也适合开发优秀的DSL
。Groovy
社区在在测试领域提供了Spock Framework,一个强大的测试库。如果你对Cucumber
很熟悉的话,可能会觉得它是一个更易用的替代品。相比Cucumber单独的feature文件和方法正则匹配机制,我认为Spock对单元测试更友好。单元测试往往由开发者驱动编写,在同一个屏幕中可以浏览会更方便管理。
root
|__src
|__test
|__resources
|__shopping.feature
|__java
|__ShoppingStepdefs.java
|__RunCukesTest.java
Cucumber的feature文件和测试执行代码是分离的,我个人认为至少在单元测试这个级别不易用
class CancelOrderCommandHandlerTest extends Specification {
def orderRepository = Mock(OrderRepository) // Spock自带了Mock支持
EventPublisher eventPublisher = Mock()
def subject = new CancelOrderCommandHandler(orderRepository, eventPublisher)
def "it should cancel the order and publish event"() {
given: "a to-be canceled order" // given描述
def order = anOrder().readyToCancel().build()
orderRepository.findBy(order.trackingId) >> order // 设置预期返回值,是不是比Mockito.when()简单呢?
when: "ask to cancel the order" // when描述
def after = subject.handle(new CancelOrderCommand(order.trackingId))
then: "the order is canceled and an event should be published" // then描述
assert after.isCanceled()
1 * eventPublisher.publish(new OrderCanceledEvent(order.trackingId)) // 相当于Mockito.verify()
}
}
Spock Interaction Based Testing 不但提供了Gerkin DSL支持,还内置了强大的Mock机制
除了Interaction Based Testing的支持,Spock
还提供了Data Driven Testing的DSL:
class TrainingCourseTest extends Specification {
def "it should tell if the class name matches the course"(String className, boolean matches) {//注意这里有参数
expect: "matches the name" // 这里是测试执行和断言
def course = aTrainingCourse().with(FUNDAMENTAL, 1).build()
assert course.matches(className) == matches
where: "training class name look like these" //这里是数据提供
className | matches //对应方法签名中的参数
"i dont care FUNDAMENTAL1 i dont care either" | true
"i dont care FUNDAMENTAL 1i dont care either" | true
"i dont care FUNDAMENTAL 1i dont care either" | true
"i dont care FUNDAMENTAL-1i dont care either" | true
"i dont care FUNDAMENTAL - 1i dont care either" | true
"i dont care FUNDAMENTAL2i dont care either" | false
"i dont care FUNDAMENTAL 2i dont care either" | false
"i dont care FUNDAMENTAL21i dont care either" | false
"i dont care FUNDAMENTAL - 2i dont care either" | false
"i dont care fundamental - 1i dont care either" | true
"i dont care 基础课程 - 1i dont care either" | true
"i dont care 基础课程1 - 1i dont care either" | true
null | false
}
}
比JUnit4 @Parameterized更友好的测试方法工厂支持
此外,Spock
还支持与spring-test
的集成,但还不成熟,在spring-boot
1.4版本引入Slice Test后,对于需要Spring Application Context
的测试还是推荐用Groovy + Junit
来写。
相比完全替换技术栈,不如考虑下混合语言编程,针对性地提升一部分代码的效能。例如测试代码,替换的风险比较低,是练手的好场景。如果你觉得Java
编写的测试代码太啰嗦,不妨试试Groovy
。