Метод ложного репозитория не вызывается внутри метода службы

Я использую Spring и тестирую с помощью JUnit5 и Mockito, чтобы протестировать метод уровня обслуживания, который вызывает метод репозитория JPA. Уровень обслуживания должен сделать запрос к базе данных, и если запись присутствует, должно быть выдано исключение.

Ниже приведены используемые классы.

ИтемСервисТест:

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

Репозиторий предметов:

public interface ItemRepository extends JpaRepository<Item, Long> {

    Optional<Item> findByItemNameIgnoreCase(String name);
}

ПредметСервис:

@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 (это класс для имитации объекта Item и его DTO):

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

Я пытался использовать Mockito, когда было следующее:

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

С помощью этой строки я ожидаю, что когда мой itemService вызывает itemRepository.findByItemNameIgnoreCase() внутри метода createItem(), он должен вернуть фиктивную запись.

Это отлично работает, когда я вызываю itemRepository непосредственно в теле тестового примера. Проблема начинается, когда я пытаюсь вызвать itemRepository на уровне сервиса, как я уже сказал. Он не возвращает ожидаемый метод When(), который ожидался, и оператор if вообще не был достигнут, и тестовый пример завершается сбоем:

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)

Итак, после этого я попытался использовать проверку, чтобы увидеть, было ли какое-либо взаимодействие с itemRepository внутри itemService, например следующее:

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

Но при этом вызове я получаю следующую ошибку:

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)

Как я могу получить доступ к оператору if, чтобы я мог утверждать, что исключение было выброшено?

Я пробовал МНОГО других решений подобных проблем здесь, в SO, но ни одно из них не сработало в моем случае, помощь в этом была бы очень признательна.

🤔 А знаете ли вы, что...
Java поддерживает интеграцию с другими языками, такими как Kotlin и Groovy.


2
53
1

Ответ:

Решено
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

    @InjectMocks
    ItemService itemService;

    @Mock
    ItemRepository itemRepository;

    // ...

    @BeforeEach
    void setUpMocks() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void test() {
      // ...
    }
}

Что происходит шаг за шагом при выполнении теста?

  1. Создан новый экземпляр ItemServiceTest.
  2. MockitoExtension инициализирует и назначает макеты объектов Mockito каждому полю, помеченному @Mock
  3. MockitoExtension создает новый экземпляр каждого поля, помеченного @InjectMocks, и внедряет макеты объектов из шага 2.
  4. Метод @BeforeEach называется
    1. MockitoAnnotations.openMocks(this) инициализирует и назначает макетные объекты Mockito каждому полю, помеченному @Mock
    2. (itemService уже присвоена ссылка, поэтому Mockito ее игнорирует)
  5. Ваш тестовый метод называется
    1. Методы заглушаются в переназначенных фиктивных экземплярах с шага 4.1.
    2. Вызывается ваша служба, вызывающая методы фиктивных экземпляров, назначенных на шаге 3.

После шага 4.1. макеты, на которые ссылаются ваши поля, и макеты, внедренные в itemService, — это разные экземпляры. Ваш тестовый метод заглушает экземпляры, на которые ссылаются поля, но ваша служба вызывает методы для экземпляров, внедренных в ваш экземпляр.

Решение:

Удалите @ExtendWith(MockitoExtension.class) или удалите MockitoAnnotations.openMocks(this) (предпочтительно).

В этом вопросе вам не обязательно доверять незнакомцам в Интернете. Добавьте в тест следующие журналы:

@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));
    // ...
}

и сервис:

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

    // ...
}

Затем вы увидите вывод, похожий на:

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

Что вы можете извлечь из этого результата?

  • ItemService создается только один раз.
  • ItemService создается с помощью экземпляра itemRepository, который назначается перед вашим методом @BeforeEach.
  • openMocks инициализирует новый экземпляр макета и переназначает поле в вашем тесте
  • Ваш тестовый метод заглушает методы на втором, переназначенном экземпляре.
  • Ваш сервис по-прежнему ссылается на первый макетный экземпляр и createItem() вызывает методы этого сервиса.

По сути, это еще одно воплощение Почему мои мокируемые методы не вызываются при выполнении модульного теста? – но не переназначая поля вручную, а заставляя Mockito переназначать новые экземпляры через openMocks(this).