0

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

Ricardo
  • 7,921
  • 14
  • 64
  • 111
  • The catalogMapper is a mock with no stubbed methods. How can it return anything other than null? Either stub `catalogMapper.convertCatalogEntityToCatalogDto` or use a real `CatalogMapper` implementation. – Lesiak Sep 29 '20 at 20:41
  • I don't understand why i should stub CatalogMapper as well. Because if I stub the catalogMapper is like I'm stubbing everything, I feel like I'm testing nothing. – Ricardo Sep 29 '20 at 21:03

1 Answers1

1

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);
    }

}
Mario Codes
  • 689
  • 8
  • 15
Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • I'd like the option3, but I have to get rid of autowire or i could just add a new constructor? – Ricardo Sep 30 '20 at 07:12
  • Add constructor, annotate it with `@Autowired`, remove `@Autowired` from fields – Lesiak Sep 30 '20 at 07:19
  • Sorry but I can't see any relation of @Spy with the initial problem. You need to be more precise if you still need advice on that. – Lesiak Sep 30 '20 at 07:47
  • Sorry, what I meant to say is, instead of get rid of @Autowired I could use spy to inject real objects, but by now is still not working :(. And sorry i provided the wrong link before https://stackoverflow.com/questions/20270391/mockito-inject-real-objects-into-private-autowired-fields – Ricardo Sep 30 '20 at 07:51
  • Added example service with constructor injection – Lesiak Sep 30 '20 at 07:56
  • Thanks for the example, and I already know how to add new constructors. But the thing is I don't want to change the code. Is something wrong using @spy? or maybe I misunderstood the use case? This is what I have https://gist.github.com/rchampa/313c944d510b2ae9f2db354e0ff2a3f7 – Ricardo Sep 30 '20 at 07:59
  • You use `@Spy` when you want real object, but substitute a few methods with stubbed calls. I can't see any object in the test which would benefit from being a `@Spy` – Lesiak Sep 30 '20 at 08:03
  • Uhm there is something that I'm misunderstanding, because I want two real objects ModelMapper and CatalogMapper and in this way (using @Spy) I could inject then into CatalogService. – Ricardo Sep 30 '20 at 08:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222293/discussion-between-lesiak-and-ricardo). – Lesiak Sep 30 '20 at 08:22