Is it possible to clone an instance of a Spring @Controller in a JUnit 5 test?
I would like to do so for the following reasons :
- Mocking pollutes the context for next tests and DirtiesContexthas a huge impact on performance. So I don't want to use Mocks unless the modification sticks to the test class.
- I would like to run tests in parallel so modifying a shared controller instance at runtime (with ReflectionUtilsfor example) will produce unpredictable behavior.
- I don't want to set a controller as prototype as it is not the case at runtime and Spring is already wiring all dependencies.
I was thinking to inject the controller in the test class with @Autowired as usual and then making a deep copy of it with SerializationUtils like described in this post but I hope there could be a more robust approach than serializing / deserializing in every test class.
What makes tests failing in parallel mode?
The end-to-end tests are using common services. For example, I have a controller used in those two tests.
@RestController
@RequestMapping("/api/public/endpointA/")
public class SomeController {
    @Autowired
    private MyService myService;
    @GetMapping("/{id}")
    public Something getSomethingById(@PathVariable int id) {
        return myService.getSomethingById(id);
    }
}
The first test just check the standard usage.
class SomeControllerTest {
    @Autowired
    @InjectMocks
    private SomeController someController;
    @Test
    void testGetSomethingById() {
        Assertions.assertEquals(
            1, 
            // I use some custom wrapper for the HTTP methods calls on 
            // the controller like this to ease the mock mvc setup.
            someController.getAndParse(
                HttpStatus.OK, 
                Something.class, 
                "/{id}", 
                1
            ).getId()
        );
    }
}
The second case test what happens when error occurs.
class SomeControllerExceptionTest {
    @Autowired
    private SomeController someController;
    @SpyBean
    private MyService myService;
    @BeforeEach
    public void before() {
        // Mock to test error case
        doThrow(RuntimeException.class).when(myService)
                .getSomethingById(anyInt());
    }
    @Test
    void testGetSomethingById() {
        Assertions.assertThrows(
            someController.getAndParse(
                 HttpStatus.OK, 
                 Something.class, 
                 "/{id}", 
                 1
            )
        );
    }
}
By mocking in the second test, I'm not sure the first test won't use the mocked instance of the second test when tests are running in parallel.
Instantiate the controller in the test method yourself, mock the dependencies inside the method and inject them in the controller.
Same situation described above but I mock on a new instance of the 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);
    }
}
Test both cases in one test.
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
            )
        );
    }
}
Yes! It's working and the context is not polluted. Ok let's say I have 10 services injected in my controller actually. I will have to do ReflectionUtils#setField 9 times for the legacy services and 1 time for the mock. Looks a bit ugly.
With AutowireCapableBeanFactory to the rescue. I've managed to clean this a little bit.
Same situation, but SomeController has 10 autowired services.
class SomeControllerTest {
    @Autowired
    private SomeController someController;
    private SomeController someControllerMocked;
    @Autowired
    private AutowireCapableBeanFactory beanFactory;
    @BeforeEach
    public void before() {
        someControllerMocked = new SomeController();
        // Replace the 9 ReflectionUtils#setField calls with this
        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
            )
        );
    }
}
I think it's the best approach I've found.
