作为一个大型电商后台业务系统,系统的稳定性和可用性有很高的要求。良好以及高覆盖的单元测试是其中重要的一环。在日常项目开发中时常会思考如何才能更加低成本,可读性高以及维护性高的单元测试方式。
目前负责的系统,作为一个衔接上下游的中间系统,有十分复杂的业务场景以及众多的RPC调用,通常一个域的系统单元测试就超过500个,所以在单元测试上从来没有停止过探索和优化。
RPC Mock
首先为了能够更好的完成RPC调用的mock,针对公司的RPC框架编写了一个对应的mock服务。 1、扩展SpringJUnitClassRunner,在createTest中初始化一个本地的RPC服务,同时执行了所有的RPC服务目标地址为本地 2、整合mockito实现对象的mock。mockito是java常用的mock框架,所以对于使用者来说机会是零成本。
BeanMock
Mock只是解决了RPC调用的问题,但是对于测试过程中很多的嵌套调用mock的场景,往往都是通过很复杂的反射方式实现。反射方式在实现上麻烦不说,最严重的就是反射设置了mock对象之后,在测试完成之后并没有将mock对象还原回真实的值。由于整个测试都是使用同一个spring上下文(通过spring注解可以实现每次都是新的上下文,但是影响测试执行效率),导致其他单元测试失败。并且这些单元测试失败的原因都是十分难排查的。
事实上一些其他的mock组件能够实现private的mock,甚至是静态对象的mock,比如power mock。 但基于整个团队二三十人都是使用mockito的情况下,转为使用另外一个mock组件,无言之间引入了新的使用以及维护成本。所以为了解决这个问题,基于spring的bean的特性,实现了Bean Mock,通过注解就能实现对嵌套对象的mock,十分简洁。详细介绍参见:BeanMock的使用介绍与说明
使用Groovy编写类DSL的单元测试
上面的两个Mock都是解决了单元测试过程中的mock问题,除了对象的mock之外,单元测试更多的工作是在测试数据的构造,场景的模拟以及最终执行结果的断言验证。这个时候groovy的语言特性以及其对java的无缝结合就派上用场。 同时参加日常项目的QA用例评审过程中,是对一个个业务场景的设计。这些测试用例可以看作特定的领域语言来描述特定的业务场景。
比如对于电商平台库存扣减的场景来说,从订单库存领域,描述一个典型的业务场景如下:
一个订单,购买两个sku。其中一个是A库存,需要3件;另外一个是B库存,需要2件。库存都占用成功。 执行订单库存扣减,验证结果:订单库存占用成功,占用明细有3个。占用明细1:占用数2 …
通过Groovy的语言特性,可以编写基于上面这个特定的领域语言,通过DSL的方式来进行单元测试,代码实例如下:
@Test
void testSingleOrderJit() throws OspException {
makeRequest {
header {
detailList {
[invType: Inv.A ]
}
}
}withOccupy OccupancyResult.success execute{
oimsService.allocateOccupancy(it)
} verify{
validateHeader {
[status:3, detailNum:1]
}
validateDetail {
[occupancyQty:-10, isShortAlloc:2]
}
}
}
}
从上面的代码看出,Groovy的语言特性在测试数据的构造以及断言上,相比java语言更为清晰与简洁。
示例中试图通过定义:
- makeRequest
- withInv
- withBrand
- withOccupancy
这样的领域名词帮助测试数据的模拟。比如在示例中就使用makeRequest创建了一个订单,有一个明细,明细是Inv.A的库存。通过withOccupancy来模拟这些库存都是能占用成功的。
通过定义execute,在execute中触发所要测试的业务方法
通过定义:
- verify
- verifyHeader
- verifyDetail
领域名词来帮助执行业务方法执行的结果验证。
上述的例子是一个很简单的DSL语言用法,希望通过逐渐完善领域的DSL,让单元测试(其实这个阶段在很多人看来属于功能测试的范畴)具有可描述的特性。更加能够表达单元测试的意图,从各种数据构造和测试验证的场景中抽离出来,专注于要验证的结果。
当然,这个并不能适用于所有的单元测试场景,应该结合实际的场景和测试目选择合适的工具。这里目的是为了提供一种思路作为参考