ModelMapper throws NPE using JUnit Mockito

960 views Asked by At

I'm having a NPE using ModelMapper

CatalogServiceTest

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class CatalogServiceTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();
    @InjectMocks private CatalogService service;
    @Mock ModelMapper modelMapper;
    @Mock CatalogMapper catalogMapper;
    @Mock CatalogRepository catalogRepository;


    @Before
    public void setUp() throws Exception {
//        MockitoAnnotations.initMocks(this);
        CatalogEntity catalogEntity = new CatalogEntity();
        catalogEntity.setId("id");
        catalogEntity.setCode("code");
        catalogEntity.setType("type");
        catalogEntity.setValue("value");

//        Optional<CatalogEntity> optionalCatalog = Optional.of(catalogEntity);
        when(catalogRepository.findByCode(any(String.class))).thenReturn(catalogEntity);
    }

    @Test
    public void whenFindByCode() {
        //Act
        CatalogDto myCatalogDto = service.findByCode("code");
        //Assert
        assertTrue(myCatalogDto.getCode().equals("code"));
    }
}

CatalogService

@Service
public class CatalogService {

    private static final Logger LOGGER = LoggerFactory.getLogger(CatalogService.class);

    @Autowired
    CatalogRepository catalogRepository;

    @Autowired
    CatalogMapper catalogMapper;

    /**
     * 
     * @param type
     * @return catalog objects which type is type
     */
    public List<CatalogDto> findByType(String type) {
        LOGGER.info("Getting catalogs by type {}", type);
        List<CatalogEntity> catalogsEntityList = catalogRepository.findByType(type);
        List<CatalogDto> catalogDtoList = new ArrayList<>();
        catalogsEntityList.forEach(catalogEntity -> {
            catalogDtoList.add(catalogMapper.convertCatalogEntityToCatalogDto(catalogEntity));
        });
        return catalogDtoList;
    }
    
    /**
     * Find catalog by code.
     * @param code
     * @return catalog
     */
    public CatalogDto findByCode(String code) {
        LOGGER.info("Getting catalogs by code {}", code);
        return catalogMapper.convertCatalogEntityToCatalogDto(catalogRepository.findByCode(code));
    }
}

CatalogMapper

@Component
public class CatalogMapper {
    @Autowired
    private ModelMapper modelMapper;
    
    /**
     * Converts CatalogEntity object to CatalogDto object
     * @param catalogEntity
     * @return converted CatalogDto object
     */
    public CatalogDto convertCatalogEntityToCatalogDto(CatalogEntity catalogEntity) {
        return modelMapper.map(catalogEntity, CatalogDto.class);
    }
}

CatalogRepository

@Repository
public interface CatalogRepository extends MongoRepository<CatalogEntity, String> {

    List<CatalogEntity> findByType(String type);

    CatalogEntity findByCode(String code);
    
}

The problem

The catalogRepository.findByCode(code) is returning a CatalogEntity object as expected and the problem comes after executing catalogMapper.convertCatalogEntityToCatalogDto(catalogRepository.findByCode(code)); that return null.

I'm using Intellij and this is the breakpoint just right before execute catalogMapper.convertCatalogEntityToCatalogDto function. enter image description here enter image description here

1

There are 1 answers

9
Lesiak On BEST ANSWER

The catalogMapper is a mock with no stubbed methods.

There are a few ways in which you can fix your test:

Option 1: only test interaction with CatalogMapper

In this option, you stub a call to catalogMapper.convertCatalogEntityToCatalogDto. This is a thin unit test, you only test interactions with collaborating services.

As you said you want to test real implementation of mapper, there are two options:

Option 2: use SpringBootTest

In this option, you rely on SpringBootTest to set up your entire application context.

You need following changes:

  • use @Autowired instead of @InjectMock to get you object under test
  • use @MockBean instead of @Mock for repository. SpringBootTest ignores @Mock.
  • get rid of other mocks
  • as it creates entire application context, I would exclude this option, unless a full integration test is your goal (you started with @SpringBootTest in your code)
@SpringBootTest
public class CatalogServiceTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();
    @Autowired
    private CatalogService service;
    @MockBean
    CatalogRepository catalogRepository;

}

Option 3: construct services you need yourself

  • get rid of @SpringBootTest
  • mock only objects you want to mock - the repository
  • create real objects for other services
  • you may need to change field injection to constructor injection in your services, which is a good idea anyway
@Service
public class CatalogService {

    final CatalogRepository catalogRepository;

    final CatalogMapper catalogMapper;

    @Autowired
    public CatalogService(CatalogRepository catalogRepository, CatalogMapper catalogMapper) {
        this.catalogRepository = catalogRepository;
        this.catalogMapper = catalogMapper;
    }
}

This approach creates only objects that are used by your test, not entire application context, so will likely result in leaner test that option 2.

@RunWith(MockitoJUnitRunner.class)
public class CatalogServiceTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private CatalogService service;
    @Mock
    CatalogRepository catalogRepository;


    @Before
    public void setUp() throws Exception {
        var modelMapper = new ModelMapper();
        var catalogMapper =new CatalogMapper(modelMapper);
        service = new CatalogService(catalogRepository, catalogMapper);

        CatalogEntity catalogEntity = new CatalogEntity();
        catalogEntity.setId("id");
        catalogEntity.setCode("code");
        when(catalogRepository.findByCode(any(String.class))).thenReturn(catalogEntity);
    }

}