如何在JUnit 5测试中克隆Spring @Controller实例避免上下文污染?
是否可以在JUnit 5测试中克隆Spring @Controller实例?
我希望这么做的原因如下:
- Mock会污染后续测试的上下文,
@DirtiesContext对性能影响极大,因此仅希望Mock修改局限于当前测试类; - 我希望并行运行测试,运行时修改共享Controller实例(如通过
ReflectionUtils)会导致不可预测的行为; - 不想将Controller设置为原型模式,因为生产环境并非如此,且Spring已完成依赖注入。
我原本考虑通过@Autowired在测试类中注入Controller,再用SerializationUtils进行深拷贝,但希望有比序列化/反序列化更健壮的方案。
并行测试失败的原因是什么?
端到端测试使用公共服务,例如以下两个测试共用一个Controller:
@RestController @RequestMapping("/api/public/endpointA/") public class SomeController { @Autowired private MyService myService; @GetMapping("/{id}") public Something getSomethingById(@PathVariable int id) { return myService.getSomethingById(id); } }
第一个测试验证标准用法:
class SomeControllerTest { @Autowired @InjectMocks private SomeController someController; @Test void testGetSomethingById() { Assertions.assertEquals( 1, // 我用了自定义封装来调用Controller的HTTP方法,简化MockMvc配置 someController.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ).getId() ); } }
第二个测试验证异常场景:
class SomeControllerExceptionTest { @Autowired private SomeController someController; @SpyBean private MyService myService; @BeforeEach public void before() { // Mock异常场景 doThrow(RuntimeException.class).when(myService) .getSomethingById(anyInt()); } @Test void testGetSomethingById() { Assertions.assertThrows( someController.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ) ); } }
并行运行时,第二个测试中的Mock可能会被第一个测试使用,导致结果不可控。
自行在测试方法中实例化Controller,Mock依赖并注入
在新的Controller实例上进行Mock,示例如下:
@RestController @RequestMapping("/api/public/endpointA/") public class SomeController { @Autowired private MyService myService; @GetMapping("/{id}") public Something getSomethingById(@PathVariable int id) { return myService.getSomethingById(id); } }
在同一个测试类中验证两种场景:
class SomeControllerTest { @Autowired private SomeController someController; private SomeController someControllerMocked; @BeforeEach public void before() { someControllerMocked = new SomeController(); MyService mockedService = mock(MyService.class); doThrow(RuntimeException.class).when(mockedService) .getSomethingById(anyInt()); ReflectionTestUtils.setField( someControllerMocked, "myService", mockedService ); } @Test void testGetSomethingById() { Assertions.assertEquals( 1, someController.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ).getId() ); } @Test void testGetSomethingByIdException() { Assertions.assertThrows( someControllerMocked.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ) ); } }
该方案可行且不会污染上下文,但如果Controller有10个依赖,需要多次调用ReflectionUtils#setField,不够优雅。
借助AutowireCapableBeanFactory优化
针对Controller有10个自动注入服务的场景,优化后的测试代码如下:
class SomeControllerTest { @Autowired private SomeController someController; private SomeController someControllerMocked; @Autowired private AutowireCapableBeanFactory beanFactory; @BeforeEach public void before() { someControllerMocked = new SomeController(); // 用这一行替代9次ReflectionUtils#setField调用 beanFactory.autowireBean(someControllerMocked); MyService mockedService = mock(MyService.class); doThrow(RuntimeException.class).when(mockedService) .getSomethingById(anyInt()); ReflectionTestUtils.setField( someControllerMocked, "myService", mockedService ); } @Test void testGetSomethingById() { Assertions.assertEquals( 1, someController.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ).getId() ); } @Test void testGetSomethingByIdException() { Assertions.assertThrows( someControllerMocked.getAndParse( HttpStatus.OK, Something.class, "/{id}", 1 ) ); } }
这是目前我找到的最佳方案。
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

