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

Я использую 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, но ни одно из них не сработало в моем случае, помощь в этом была бы очень признательна.

Можете ли вы сократить код до наименьшего рабочего примера, воспроизводящего проблему? Т.е. удалите все, что не связано с вызовом findByItemNameIgnoreCase, и проверьте. Это значительно облегчит отладку (и часто может решить проблему, поскольку сделает ее более заметной для вас).

Torben 07.08.2024 06:41

Вам не нужны MockitoAnnotations.openMocks(this); и @ExtendWith(MockitoExtension.class)

tgdavies 07.08.2024 08:19

Несвязано, но почему вы путаете when(…) и given(…)? Зачем использовать два API, которые делают одно и то же в одном файле (даже один и тот же метод)

knittl 07.08.2024 08:36

@tgdavies не только вам это не нужно, вы не должны использовать оба подхода вместе

knittl 07.08.2024 10:09

@tgdavies Я использовал заданный(...), чтобы проверить, сохранилась ли запись в базе данных в другом тестовом примере, и я подумал, что мне нужно использовать и в этом тесте. Но в конце концов, как вы сказали, это оказалось излишним.

felipesousac 08.08.2024 01:52
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
5
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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).

Вы не только ответили на мой вопрос, но и преподали мне урок. Благодаря вашим изменениям тестовый пример теперь работает как положено. Раньше я пробовал отладку и заметил, что было создано два экземпляра itemRepository, но до вашего ответа я не знал, что вызывает эту проблему.

felipesousac 08.08.2024 01:56

Другие вопросы по теме