Spock 2.0-M1-groovy-2.5中Mock类后仍触发真实方法内部空指针异常(NPE)的解决方法
这个问题我之前也碰到过,核心原因是Kotlin中类成员方法默认是final的,而Spock默认只能Mock非final方法,导致你的Mock并没有真正拦截index方法的调用,实际执行了原类的index逻辑,而Mock对象的restHighLevelClient属性是null,所以触发了空指针异常。
问题拆解
你虽然把ReactiveRestHighLevelClient类声明为open了,但Kotlin的方法默认是final的——也就是说,index方法并没有被标记为可继承/可Mock的。Spock基于字节码生成技术(默认是CGLIB),无法拦截final方法的调用,所以当你调用reactiveElasticsearchClient.index(...)时,实际跑的是原类的代码,而Mock对象里的restHighLevelClient没有被初始化,自然就抛出NPE了。
解决方案
这里有几种可行的解决方式,按推荐程度排序:
1. 给Kotlin方法添加open关键字(最简单直接)
修改ReactiveRestHighLevelClient的index方法,加上open修饰符,让它变成可Mock的:
open class ReactiveRestHighLevelClient(val restHighLevelClient: RestHighLevelClient, private val objectMapper: ObjectMapper) { // 给方法加上open关键字 open fun index(indexRequest: IndexRequest): Mono<IndexResponse> { return Mono.create<IndexResponse> { sink -> restHighLevelClient.indexAsync( indexRequest, object : ActionListener<IndexResponse> { override fun onFailure(e: Exception) { e.printStackTrace() sink.error(e) } override fun onResponse(response: IndexResponse) { sink.success(response) } } ) } } }
这样Spock就能正常拦截这个方法的调用,不会再执行原方法的逻辑,你的Mock行为reactiveElasticsearchClient.index(_ as IndexRequest) >> Mono.just(indexResponse)就能生效了。
2. 抽象出接口(最佳实践)
从依赖注入和解耦的角度,更推荐给ReactiveRestHighLevelClient定义一个接口,让类实现这个接口,然后在测试中Mock接口而非具体实现类:
// 定义接口 interface ReactiveElasticsearchClient { fun index(indexRequest: IndexRequest): Mono<IndexResponse> } // 实现类 open class ReactiveRestHighLevelClient( val restHighLevelClient: RestHighLevelClient, private val objectMapper: ObjectMapper ) : ReactiveElasticsearchClient { override fun index(indexRequest: IndexRequest): Mono<IndexResponse> { // 原逻辑不变 } }
然后修改你的服务类依赖接口:
@Component class ThreadModelElasticsearchService( private val objectMapper: ObjectMapper, private val reactiveElasticsearchClient: ReactiveElasticsearchClient, // 改为依赖接口 private val extraFactsService: ExtraFactsService, private val customerDataService: CustomerDataService, rateLimiterRegistry: RateLimiterRegistry ) { // 业务逻辑不变 }
测试类中Mock接口:
class ThreadModelElasticsearchServiceTest extends Specification { ReactiveElasticsearchClient reactiveElasticsearchClient = Mock() // Mock接口 // 其他代码不变 }
这种方式不仅解决了Mock问题,还让代码更符合面向接口编程的原则,后续替换实现类也更方便。
3. 配置Spock使用ByteBuddy Mock final方法(无需修改源代码)
如果不想改动业务代码,可以通过配置让Spock使用ByteBuddy来支持Mock final方法。Spock 2.x已经内置了这个功能,你可以通过两种方式开启:
方式一:添加配置文件
在src/test/resources下创建spock.configuration文件,内容如下:
spock.mock.useByteBuddyMocking = true
方式二:使用注解
在测试类上添加@MockConfig注解:
@MockConfig(useByteBuddy = true) class ThreadModelElasticsearchServiceTest extends Specification { // 测试代码不变 }
这样Spock就能拦截final方法的调用,你的Mock行为就会生效了。
验证
不管用哪种方式,修改后重新运行测试,Mock的index方法会被正确拦截,不会再执行原方法的逻辑,也就不会触发NPE了。
内容的提问来源于stack exchange,提问作者Clyde Barrow

