Я использую 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, но ни одно из них не сработало в моем случае, помощь в этом была бы очень признательна.
Вам не нужны MockitoAnnotations.openMocks(this);
и @ExtendWith(MockitoExtension.class)
Несвязано, но почему вы путаете when(…)
и given(…)
? Зачем использовать два API, которые делают одно и то же в одном файле (даже один и тот же метод)
@tgdavies не только вам это не нужно, вы не должны использовать оба подхода вместе
@tgdavies Я использовал заданный(...), чтобы проверить, сохранилась ли запись в базе данных в другом тестовом примере, и я подумал, что мне нужно использовать и в этом тесте. Но в конце концов, как вы сказали, это оказалось излишним.
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
@InjectMocks
ItemService itemService;
@Mock
ItemRepository itemRepository;
// ...
@BeforeEach
void setUpMocks() {
MockitoAnnotations.openMocks(this);
}
@Test
void test() {
// ...
}
}
Что происходит шаг за шагом при выполнении теста?
ItemServiceTest
.MockitoExtension
инициализирует и назначает макеты объектов Mockito каждому полю, помеченному @Mock
MockitoExtension
создает новый экземпляр каждого поля, помеченного @InjectMocks
, и внедряет макеты объектов из шага 2.@BeforeEach
называется
MockitoAnnotations.openMocks(this)
инициализирует и назначает макетные объекты Mockito каждому полю, помеченному @Mock
itemService
уже присвоена ссылка, поэтому Mockito ее игнорирует)После шага 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
Что вы можете извлечь из этого результата?
itemRepository
, который назначается перед вашим методом @BeforeEach
.openMocks
инициализирует новый экземпляр макета и переназначает поле в вашем тестеcreateItem()
вызывает методы этого сервиса.По сути, это еще одно воплощение Почему мои мокируемые методы не вызываются при выполнении модульного теста? – но не переназначая поля вручную, а заставляя Mockito переназначать новые экземпляры через openMocks(this)
.
Вы не только ответили на мой вопрос, но и преподали мне урок. Благодаря вашим изменениям тестовый пример теперь работает как положено. Раньше я пробовал отладку и заметил, что было создано два экземпляра itemRepository, но до вашего ответа я не знал, что вызывает эту проблему.
Можете ли вы сократить код до наименьшего рабочего примера, воспроизводящего проблему? Т.е. удалите все, что не связано с вызовом findByItemNameIgnoreCase, и проверьте. Это значительно облегчит отладку (и часто может решить проблему, поскольку сделает ее более заметной для вас).