1

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 DirtiesContext has 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 ReflectionUtils for 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.

Paul
  • 1,410
  • 1
  • 14
  • 30
  • what makes your controller stateful? You mentioned that you don't want to mark it with scope prototype because it's not the case in the deployment runtime. so can you elaborate on why your controller state is mutating during test execution? – Anatolii Vakaliuk Dec 20 '22 at 17:12
  • @AnatoliiVakaliuk in fact the controller is not stateful. It's more about arguing with the lead dev to make the change to a prototype scope for this testing purpose. – Paul Dec 20 '22 at 17:30
  • I see. The question is what makes your tests fail in parallel mode? is there a data dependency between those tests? (e.g. test 1 verifies some POST request that mutates database state that is seen by test 2 that verifies GET operation) – Anatolii Vakaliuk Dec 20 '22 at 17:36
  • 2
    What do you want to test and how? Is it just the controller? Instantiate it in the test method yourself, mock the dependencies inside the method and inject them in the controller, setup test. Basically move the setup to the test method. The fact that you are using Spring in your project doesn't mean that each and every test needs to be a spring based test. If you want more help please show some test code you have now and which bits are problematic instead of describing it. – M. Deinum Dec 20 '22 at 19:30
  • @AnatoliiVakaliuk In fact it's more a context dependency that makes the tests failing in parallel. I've edited the question to answers yours. @M.Deinum put me on the right track to find an acceptable solution in addition with `AutowireCapableBeanFactory` . – Paul Dec 21 '22 at 09:53

0 Answers0