I am big on clean well-isolated unit tests. But I am stumbling on the "clean" part here for testings a controller that uses DomainClassConverter
feature to get entities as parameters for its mapped methods.
@Entity
class MyEntity {
@Id
private Integer id;
// rest of properties goes here.
}
The controller is defined like this
@RequestMapping("/api/v1/myentities")
class MyEntitiesController {
@Autowired
private DoSomethingService aService;
@PostMapping("/{id}")
public ResponseEntity<MyEntity> update(@PathVariable("id")Optional<MyEntity> myEntity) {
// do what is needed here
}
}
So from the DomainClassConverter
small documentation I know that it uses CrudRepository#findById
to find entities. What I would like to know is how can I mock that cleanly in a test. I have had some success by doing this steps:
- Create a custom Converter/Formatter that I can mock
- Instantiate my own MockMvc with above converter
- reset mock and change behaviour at each test.
The problem is that the setup code is complex and thus hard to debug and explain (my team is 99% junior guys coming from rails or uni so we have to keep things simple). I was wondering if there is a way to inject the desired MyEntity
instances from my unit test while keep on testing using the @Autowired
MockMvc
.
Currently I am trying to see if I can inject a mock of the CrudRepository
for MyEntity
but no success. I have not worked in Spring/Java in a few years (4) so my knowledge of the tools available might not be up to date.
So from the DomainClassConverter small documentation I know that it uses CrudRepository#findById to find entities. What I would like to know is how can I mock that cleanly in a test.
You will need to mock 2 methods that are called prior the CrudRepository#findById
in order to return the entity you want. The example below is using RestAssuredMockMvc
, but you can do the same thing with MockMvc if you inject the WebApplicationContext
as well.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SomeApplication.class)
public class SomeControllerTest {
@Autowired
private WebApplicationContext context;
@MockBean(name = "mvcConversionService")
private WebConversionService webConversionService;
@Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
SomeEntity someEntity = new SomeEntity();
when(webConversionService.canConvert(any(TypeDescriptor.class), any(TypeDescriptor.class)))
.thenReturn(true);
when(webConversionService.convert(eq("1"), any(TypeDescriptor.class), any(TypeDescriptor.class)))
.thenReturn(someEntity);
}
}
At some point Spring Boot will execute the WebConversionService::convert
, which will later call DomainClassConverter::convert
and then something like invoker.invokeFindById
, which will use the entity repository to find the entity.
So why mock WebConversionService
instead of DomainClassConverter
? Because DomainClassConverter
is instantiated during application startup without injection:
DomainClassConverter<FormattingConversionService> converter =
new DomainClassConverter<>(conversionService);
Meanwhile, WebConversionService
is a bean which will allow us to mock it:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
It is important to name the mock bean as mvcConversionService
, otherwise it won't replace the original bean.
Regarding the stubs, you will need to mock 2 methods. First you must tell that your mock can convert anything:
when(webConversionService.canConvert(any(TypeDescriptor.class), any(TypeDescriptor.class)))
.thenReturn(true);
And then the main method, which will match the desired entity ID defined in the URL path:
when(webConversionService.convert(eq("1"), any(TypeDescriptor.class), any(TypeDescriptor.class)))
.thenReturn(someEntity);
So far so good. But wouldn't be better to match the destination type as well? Something like eq(TypeDescriptor.valueOf(SomeEntity.class))
? It would, but this creates a new instance of a TypeDescriptor, which will not match when this stub is called during the domain conversion.
This was the cleanest solution I've put to work, but I know that it could be a lot better if Spring would allow it.