Search code examples
javaspring-bootunit-testingtestingmapstruct

Cannot use MapStruct in Unit Test for a service method


I write unit test for testing mappings and it works without any problem. However, when I try to test a service method that makes mapping via MapperStruct, the mapper instance throws "Null pointer exception". Here is the approach I am following (before that, I have tried many different ways as suggested on Stackoverflow, but none of them has worked :(

UnitTest:

@SpringBootTest // ???
@ExtendWith(MockitoExtension.class)
class CategoryServiceTest {

    @InjectMocks
    private CategoryService service;

    @Mock
    private CategoryRepository categoryRepository;

    @Mock
    public CategoryRequestMapper categoryRequestMapper;


    @ParameterizedTest
    @CsvFileSource(resources = "/data/categories.csv")
    void test_create(Long id, String name, Integer ordinal) {
        CategoryRequest request = new CategoryRequest(id, name, ordinal);
           when(categoryRepository.existsByNameIgnoreCase(request.getName()))
    .thenReturn(false);

        service.create(request);

        // validations ...
    }
}

ServiceMethod:

@Service
@RequiredArgsConstructor
public class CategoryService {

    private final CategoryRepository categoryRepository;
    private final CategoryRequestMapper categoryRequestMapper;

    public CommandResponse create(CategoryRequest request) {

        // !!! categoryRequestMapper is null
        final Category category = categoryRequestMapper.toEntity(request);
        categoryRepository.save(category);
        return CommandResponse.builder().id(category.getId()).build();
    }
}

So, should I also stub categoryRequestMapper in the test method?

Update:*

@SpringBootTest(classes = {CategoryController.class, CategoryService.class, CategoryResponseMapperImpl.class})
class CategoryControllerTest {

    @Autowired
    private CategoryController categoryController;

    @MockBean
    private CategoryRepository categoryRepository;

    @MockBean
    private CategoryService categoryService;

    @Spy
    // @SpyBean
    public CategoryResponseMapper categoryResponseMapper = new CategoryResponseMapperImpl();

    @Test
    void test_findById() throws Exception {
        LocalDateTime dateTime = LocalDate.of(2022, 1, 1).atStartOfDay();

        Category category = new Category();
        category.setId(123L);
        category.setName("Category");
        category.setOrdinal(1);

        when(clock.instant()).thenReturn(dateTime.atZone(ZoneId.of("UTC")).toInstant());
        when(categoryRepository.findById(any())).thenReturn(Optional.of(category));

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/api/v1/categories/{id}", 123L);
        MockMvcBuilders.standaloneSetup(categoryController)
                .build()
                .perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().contentType("application/json"))
                .andExpect(MockMvcResultMatchers.content()
                        .string(
                                "{\"timestamp\":1640995200000,\"message\":\"Success\",\"data\":{\"id\":123,\"name\":\"Category\",\"ordinal\":1}}"));
    }
}
@GetMapping("/categories/{id}")
public ResponseEntity<ApiResponse<CategoryResponse>> findById(@PathVariable long id) {
    final CategoryResponse response = categoryService.findById(id);
    return ResponseEntity.ok(new ApiResponse<>(Instant.now(clock).toEpochMilli(), SUCCESS, response));
}
private final CategoryRepository categoryRepository;
private final CategoryRequestMapper categoryRequestMapper;
private final CategoryResponseMapper categoryResponseMapper;

public CategoryResponse findById(Long id) {
    return categoryRepository.findById(id)
            .map(categoryResponseMapper::toDto)
            .orElseThrow(() -> {
                return new NoSuchElementFoundException(NOT_FOUND);
            });
}

Solution

  • Unit Test

    For a unit test, try using a spy instead. By using a spy, the real instance will be wrapped by Mockito so that you can stub or verify against it, but when not stubbed, the real methods will be executed when called. By annotating it with @Spy, Mockito will consider it as well for injecting the dependencies in the class annotated with @InjectMocks.

    Example

    @ExtendWith(MockitoExtension.class)
    class CategoryServiceTest {
    
        @InjectMocks
        private CategoryService service;
    
        @Mock
        private CategoryRepository categoryRepository;
    
        @Spy
        private CategoryRequestMapper categoryRequestMapper = new CategoryRequestMapperImpl();
    
        // your tests go here
    

    Integration Test

    If your mapper needs other autowired beans (like other mappers) as well, you might want to change it to a reduced integration test. You can instruct Spring to create an application context with only the beans you specify instead of the whole application context with all auto configurations etc. If you want to verify that your mapper was called, you can use @SpyBean like in the example below. If you don't want to verify or stub it, you can omit it as well.

    Example

    @SpringBootTest(classes = {CategoryService.class, CategoryRequestMapper.class, ...}) // other beans that should be automatically created by Spring go here
    class CategoryServiceTest {
    
        @Autowired
        private CategoryService service;
    
        @MockBean
        private CategoryRepository categoryRepository;
    
        @SpyBean
        private CategoryRequestMapper categoryRequestMapper
    
        // your tests go here