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.