Kotlin自定义SQS消费者Channel迭代器部分覆盖测试问题
自定义SQS消费者单元测试部分覆盖问题排查
问题描述
实现的自定义SQS消费者采用如下架构:
- 单个协程负责周期性拉取SQS消息
- 多个Worker以非阻塞方式并行处理收到的消息
- 基于Channel实现MsgReceiver拉取协程和Worker之间的通信
编写单元测试时,kover、Sonar插件均无法统计到100%测试覆盖率,插件高亮标记遍历消息的while循环为部分覆盖,无法定位遗漏的测试场景。
当前已覆盖的测试场景:
- 单消息处理场景
- 拉取结果为空的无消息场景
- 单次拉取10条以上消息的批量场景
- 单次轮询完成后无更多消息的退出场景
- 业务异常处理场景
排查过程中查看itr.hasNext()源码发现方法内部存在多个分支,疑惑为何覆盖率统计会将依赖库的内部方法分支纳入自身代码的覆盖率统计范围。
解答
首先明确:不存在覆盖率工具把依赖库代码计入项目覆盖率的情况。
kover、Sonar这类覆盖率工具底层基于字节码插桩实现统计,只会对你项目自身编译产出的class文件做插桩修改,第三方依赖的字节码不会被处理,根本不可能把依赖内部的分支算到你的覆盖率结果里。
你看到while行被标记为部分覆盖,统计的是你自己写的这行while语句编译后生成的字节码跳转分支,和itr.hasNext()的内部实现没有关系。你写的while(itr.hasNext())编译成字节码后,本身就会生成多组跳转指令:根据hasNext()的返回结果决定是进入循环体,还是跳出循环执行后续逻辑,这些跳转指令属于你的代码编译产物,自然会被纳入覆盖率统计。你翻源码看到hasNext()内部有多个分支,只是依赖方法自身的实现逻辑,和你的覆盖率统计结果无关。
针对协程+Channel的技术栈,while循环出现部分覆盖,90%以上的概率是漏了以下两类场景:
- 你覆盖的「无消息场景」是Channel初始为空、第一次调用
hasNext()就直接返回false、整个循环完全没进入的情况,但没有覆盖Channel迭代器的挂起恢复分支。Channel的hasNext()是挂起函数,编译后会额外生成挂起/恢复的跳转分支,如果你写测试时直接用固定列表mock迭代器返回,没有走真实Channel的「暂时无消息->hasNext()挂起等待->收到新消息恢复执行进入循环->所有消息处理完成后Channel关闭退出」的完整流程,这个挂起分支就不会被覆盖。 - 没有覆盖协程取消退出的分支。运行在协程作用域内的循环,在协程被取消时,
hasNext()会抛出CancellationException直接中断循环,如果你之前覆盖的异常场景只有业务逻辑异常,没有测试过主动取消协程导致循环退出的路径,也会出现部分覆盖的提示。
补全两个测试场景即可达到100%覆盖率:
- 不使用mock迭代器,走真实Channel通信流程:启动MsgReceiver拉取协程和Worker,先往Channel发送1条消息,等Worker处理完成后再关闭Channel,等待协程正常退出,即可覆盖挂起恢复、循环执行后正常判断退出的分支。
- 新增协程取消测试用例:启动消费协程运行循环逻辑,在循环运行过程中主动取消协程作用域,等待协程退出,覆盖协程取消的分支。
内容的提问来源于stack exchange,提问作者Pranav
相关产品推荐
相关产品推荐

