javaspringunit-testingjunitmockito

Mock repository method not being called inside service method


I'm using Spring and testing with JUnit5 and mockito to test a service layer method that makes a call to a JPA repository method. The service layer should make a query to the database and if a record is present then an exception must be throw.

Bellow the classes that are being used.

ItemServiceTest:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

    MockItem input;

    @InjectMocks
    ItemService itemService;

    @Mock
    ItemRepository itemRepository;

    @Mock
    CategorieRepository categorieRepository;

    @Mock
    ItemDTOMapper itemDTOMapper;
    
    @Mock
    private UriComponentsBuilder uriBuilder;

    @Mock
    private UriComponents uriComponents;

    @Captor
    private ArgumentCaptor<Long> longCaptor;

    @Captor
    private ArgumentCaptor<String> stringCaptor;

    @BeforeEach
    void setUpMocks() {
        input = new MockItem();
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testCase() throws ItemAlreadyCreatedException {
        Item item = input.mockEntity();
        CreateItemData data = input.mockDTO();
        ItemListData listData = input.mockItemListData();

        when(itemRepository.findByItemNameIgnoreCase(any())).thenReturn(Optional.of(item));
        given(uriBuilder.path(stringCaptor.capture())).willReturn(uriBuilder);
        given(uriBuilder.buildAndExpand(longCaptor.capture())).willReturn(uriComponents);

        Exception ex = assertThrows(ItemAlreadyCreatedException.class, () -> {
            itemService.createItem(data, uriBuilder);
        });

        String expectedMessage = "There is an item created with this name";
        String actualMessage = ex.getMessage();

        assertEquals(expectedMessage, actualMessage);
    }
}

ItemRepository:

public interface ItemRepository extends JpaRepository<Item, Long> {

    Optional<Item> findByItemNameIgnoreCase(String name);
}

ItemService:

@Service
public class ItemService {

    private final ItemRepository itemRepository;
    private final CategorieRepository categorieRepository;
    private final ItemDTOMapper itemDTOMapper;
    private final ImageService imageService;

    public ItemService(ItemRepository itemRepository, CategorieRepository categorieRepository, ItemDTOMapper itemDTOMapper, ImageService imageService) {
        this.itemRepository = itemRepository;
        this.categorieRepository = categorieRepository;
        this.itemDTOMapper = itemDTOMapper;
        this.imageService = imageService;
    }
    
    @Transactional
    public CreateRecordUtil createItem(CreateItemData data, UriComponentsBuilder uriBuilder) throws ItemAlreadyCreatedException {
        
        Optional<Item> isNameInUse = itemRepository.findByItemNameIgnoreCase(data.itemName());

        if (isNameInUse.isPresent()) {
            throw new ItemAlreadyCreatedException("There is an item created with this name");
        }

        //some logic after if statement
 
        return new CreateRecordUtil();
    }
}

MockItem (it is a class to mock Item entity and its DTOs):

public class MockItem {

    public Item mockEntity() {
        return mockEntity(0);
    }

    public CreateItemData mockDTO() {
        return mockDTO(0);
    }

    public ItemListData mockItemListData() {
        return itemListData(0);
    }

    public Item mockEntity(Integer number) {
        Item item = new Item();
        Categorie category = new Categorie(11L, "mockCategory", "mockDescription");

        item.setId(number.longValue());
        item.setItemName("Name Test" + number);
        item.setDescription("Name Description" + number);
        item.setCategory(category);
        item.setPrice(BigDecimal.valueOf(number));
        item.setNumberInStock(number);

        return item;
    }

    public CreateItemData mockDTO(Integer number) {
        CreateItemData data = new CreateItemData(
                "Name Test" + number,
                "Name Description" + number,
                11L,
                BigDecimal.valueOf(number),
                number);

        return data;
    }

    private ItemListData itemListData(Integer number) {
        CategoryListData category = new CategoryListData(11L, "mockCategory");

        ItemListData data = new ItemListData(
                number.longValue(),
                "First Name Test" + number,
                category,
                "Name Description" + number,
                BigDecimal.valueOf(number),
                number
        );

        return data;
    }
}

I've tried to use mockito when like the following:

when(itemRepository.findByItemNameIgnoreCase(any())).thenReturn(Optional.of(item));

With this line I expect that when my itemService calls itemRepository.findByItemNameIgnoreCase() inside createItem() method, it should return the mock record.

That works fine when I call itemRepository directly in the test case body, the problem begins when I tried to call itemRepository in the service layer as I said. It does not returned the expected when() that was being expected and the if statement was not reached at all, and the test case fails with:

org.opentest4j.AssertionFailedError: Expected com.inventory.server.infra.exception.ItemAlreadyCreatedException to be thrown, but nothing was thrown.

    at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:73)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
    at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3115)
    at com.inventory.server.service.ItemServiceTest.testCase(ItemServiceTest.java:84)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

So, after that I've tried to use verify to see if there were some interaction being made with itemRepository inside itemService, like the following:

verify(itemRepository).findByItemNameIgnoreCase(any());

But with that call I get the following error:

Wanted but not invoked:
itemRepository.findByItemNameIgnoreCase(
    <any>
);
-> at com.inventory.server.service.ItemServiceTest.testCase(ItemServiceTest.java:92)
Actually, there were zero interactions with this mock.

Wanted but not invoked:
itemRepository.findByItemNameIgnoreCase(
    <any>
);
-> at com.inventory.server.service.ItemServiceTest.testCase(ItemServiceTest.java:92)
Actually, there were zero interactions with this mock.

    at com.inventory.server.service.ItemServiceTest.testCase(ItemServiceTest.java:92)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

How can I reach the if statement so I can assert that the exception was throw?

I've tried A LOT of other similar problems solutions here in SO, but none of then worked in my case, a help in this one would be really appreciated.


Solution

  • @ExtendWith(MockitoExtension.class)
    class ItemServiceTest {
    
        @InjectMocks
        ItemService itemService;
    
        @Mock
        ItemRepository itemRepository;
    
        // ...
    
        @BeforeEach
        void setUpMocks() {
            MockitoAnnotations.openMocks(this);
        }
    
        @Test
        void test() {
          // ...
        }
    }
    

    What is happening step by step when executing your test?

    1. A new instance of ItemServiceTest is created
    2. MockitoExtension initializes and assigns Mockito mock objects to each field annotated with @Mock
    3. MockitoExtension creates a new instance of each field annotated with @InjectMocks and injects the mock objects from step 2
    4. The @BeforeEach method is called
      1. MockitoAnnotations.openMocks(this) initializes and assigns Mockito mock objects to each field annotated with @Mock
      2. (itemService already has a reference assigned, so it is ignored by Mockito)
    5. Your test method is called
      1. Methods are stubbed on the reassigned mock instances from step 4.1
      2. Your service is called, invoking methods on the mock instances assigned in step 3

    After step 4.1. the mocks referenced by your fields and the mocks injected into itemService are different instances. Your test method stubs the instances referenced by the fields, but your service invokes methods on the instances injected into your instance.

    Solution:

    Remove @ExtendWith(MockitoExtension.class) or remove MockitoAnnotations.openMocks(this) (preferred).

    You don't have to trust strangers on the internet on this one. Add the following logs to your test:

    @BeforeEach
    void setUpMocks() {
        input = new MockItem();
        System.out.println("before openMocks " + System.identityHashCode(itemRepository));
        MockitoAnnotations.openMocks(this);
        System.out.println("after openMocks" + System.identityHashCode(itemRepository));
    }
    
    
    @Test
    void testCase() throws ItemAlreadyCreatedException {
        System.out.println("testCase() " + System.identityHashCode(itemRepository));
        // ...
    }
    

    and service:

    public ItemService(ItemRepository itemRepository, CategorieRepository categorieRepository, ItemDTOMapper itemDTOMapper, ImageService imageService) {
        System.out.println("new ItemService() " + System.identityHashCode(itemRepository));
    
        this.itemRepository = itemRepository;
        this.categorieRepository = categorieRepository;
        this.itemDTOMapper = itemDTOMapper;
        this.imageService = imageService;
    }
    
    @Transactional
    public Object createItem(CreateItemData data, UriComponentsBuilder uriBuilder) throws ItemAlreadyCreatedException {
        System.out.println("createItem() " +  System.identityHashCode(itemRepository));
    
        // ...
    }
    

    You will then see output similar to:

    new ItemService() 930641076  // step 3
    before openMocks 930641076   // step 4
    after openMocks 280541440    // step 4.1
    testCase() 280541440         // step 5
    createItem() 930641076       // step 5.2
    

    What can you derive from that output?

    In essence, this is another incarnation of Why are my mocked methods not called when executing a unit test? – but not manually reassigning the fields, but having Mockito reassign new instances via openMocks(this).