You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 18:40:59