Spring Boot MVC项目CRUD控制器单元测试报StackOverflowError原因排查
Spring Boot MVC 单元测试栈溢出问题定位与修复
你看到的满屏Logback类的报错栈,根本不是故障根源,只是Logback在打印已经无限递归的异常时的连锁反应,真正的问题出在测试打桩逻辑和控制器调用流程的错配上。
完整触发链路
顺着你的控制器代码走一遍测试执行流程,就能顺清楚栈溢出的来龙去脉:
- 测试发起
DELETE /pessoas/89L请求,进入控制器删除方法 - 控制器第一步调用
pessoaRepository.getOne(89L),但你写测试时完全没给这个方法打桩,Mockito对无打桩的方法默认返回null - 控制器拿到null值后,继续执行
pessoaRepository.delete(null) - 你之前给delete方法设置的打桩规则是「收到你测试里手动新建的那个id=89的Pessoa对象时,执行无操作逻辑」,现在实际传的是null,根本匹配不上这个规则
- 当入参不匹配打桩规则时,Mockito会抛出参数不匹配异常,异常信息里需要对比预期入参和实际入参的差异,就要把你作为打桩参数传入的Pessoa对象序列化打印。如果你的Pessoa实体存在双向关联(比如Pessoa关联Endereco,Endereco又反向关联Pessoa),调用toString时就会无限循环拼接关联属性,直接占满JVM方法栈
- 后续Logback捕获到这个已经无限递归的异常时,会反复递归构造异常代理、拼接异常栈文本,最终你看到的报错栈全是Logback相关方法,很容易误导人去调整日志配置。
具体修复方法
1. 补全所有被调用方法的打桩逻辑
对照控制器的真实调用流程,把所有会触发的Repository方法都做打桩,不要漏写。比如这个删除接口的正确测试实现:
@Test public void deletarPessoa_quandoDelete() throws Exception { Long alvoId = 89L; Pessoa pessoa = new Pessoa(); pessoa.setNome("Test Nome"); pessoa.setId(alvoId); // 严格按照控制器的调用顺序打桩 when(pessoaRepository.getOne(alvoId)).thenReturn(pessoa); doNothing().when(pessoaRepository).delete(pessoa); mockMvc.perform(delete("/pessoas/" + alvoId) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()); }
这个调整的本质是让Mockito完全模拟出控制器预期拿到的参数,从根源上避免参数不匹配、对象为null的问题。
2. 加通用防护避免同类问题重复出现
- 检查所有JPA实体类的
toString()实现,对双向关联的字段做截断,只打印关联对象的ID值,不要全量展开打印关联对象的所有属性,从根源切断双向引用导致的递归 - 单元测试开启Mockito严格桩校验模式,只要出现漏打桩、打桩入参和实际调用不匹配的情况,Mockito会直接抛出明确的定位异常,不会等到递归栈溢出才暴露问题
- 给Logback配置异常栈的最大打印深度,就算碰到超长的异常栈,也不会因为反复拼接栈文本占满内存,配置参考:
<conversionRule conversionWord="ex" converterClass="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter" /> <property name="LOG_MAX_STACK_DEPTH" value="100" />
内容的提问来源于stack exchange,提问作者Gabriel Pulga
相关产品推荐
相关产品推荐

