Spring Boot测试中MvcUriComponentsBuilder调用报No WebApplicationContext异常
这个问题的核心在于你测试控制器的方式不对,以及MvcUriComponentsBuilder.fromMappingName的工作机制:
为什么会报错?
MvcUriComponentsBuilder.fromMappingName需要依赖WebApplicationContext来根据方法名(这里的"getById")查找对应的请求映射。但你现在是直接调用控制器的add方法,手动传入UriComponentsBuilder.newInstance()——这个实例不是由Spring容器管理的,也没有关联到测试的Web上下文,所以方法调用时找不到所需的上下文,就抛出了IllegalArgumentException。
而且@WebMvcTest是切片测试,虽然会初始化Spring MVC相关的上下文,但直接调用控制器方法时,并不会触发Spring的参数注入逻辑,你相当于在普通Java方法调用里传了个空的UriComponentsBuilder,自然无法支撑fromMappingName的功能。
正确的测试方式:用MockMvc模拟请求
最优解是使用Spring提供的MockMvc来模拟HTTP请求,这样Spring会自动处理参数注入(包括UriComponentsBuilder),并且MvcUriComponentsBuilder能正常获取到Web上下文。修改你的测试代码如下:
@ExtendWith(SpringExtension.class) @WebMvcTest(PersonController.class) class PersonControllerTest { @Autowired private MockMvc mockMvc; @Test void add() throws Exception { PersonForCreate requestBody = new PersonForCreate("Charles", "Darwin"); mockMvc.perform(post("/") // 对应你的@PostMapping路径 .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isCreated()) .andExpect(header().exists("Location")); } // 如果你需要ObjectMapper,可以自动注入 @Autowired private ObjectMapper objectMapper; }
这样测试会模拟真实的HTTP请求,Spring会正确注入UriComponentsBuilder,MvcUriComponentsBuilder.fromMappingName也能正常工作,同时还能验证响应状态码和Location头是否符合预期。
替代方案:直接调用控制器方法的修复(不推荐)
如果你一定要直接调用控制器的add方法,需要手动给MvcUriComponentsBuilder设置WebApplicationContext,并且传入Spring管理的UriComponentsBuilder实例:
@ExtendWith(SpringExtension.class) @WebMvcTest(PersonController.class) class PersonControllerTest { @Autowired private PersonController personController; @Autowired private WebApplicationContext webApplicationContext; @Autowired private UriComponentsBuilder uriComponentsBuilder; @Test void add() { // 手动设置WebApplicationContext到MvcUriComponentsBuilder MvcUriComponentsBuilder.setWebApplicationContext(webApplicationContext); ResponseEntity<Person> response = this.personController.add( new PersonForCreate("Charles", "Darwin"), uriComponentsBuilder ); // 验证响应 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getHeaders().getLocation()).isNotNull(); } }
不过这种方式跳过了Spring MVC的请求处理流程,测试的价值不如用MockMvc的方式高,只适合某些特殊场景。
额外优化:简化URI生成代码
另外,你可以简化控制器里的URI生成代码,用MvcUriComponentsBuilder.fromMethodCall来替代fromMappingName,这样不需要依赖方法名查找,代码更直观:
@PostMapping public ResponseEntity<Person> add(@RequestBody final PersonForCreate personForCreate, UriComponentsBuilder uriComponentsBuilder) { Person newPerson = new Person(this.people.size() + 1, personForCreate.getFirstName(), personForCreate.getLastName()); this.people.add(newPerson); URI uri = MvcUriComponentsBuilder.fromMethodCall( MvcUriComponentsBuilder.on(PersonController.class).getById(newPerson.getId()) ).build().toUri(); return ResponseEntity.created(uri).body(newPerson); }
这种方式通过方法引用直接定位到getById方法,不需要依赖Web上下文查找方法名,在测试和生产环境都能更稳定工作。
内容的提问来源于stack exchange,提问作者Mekswoll

