Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Вивчайте Mockito | Тестування Бекенд-Додатків
Spring Boot Backend

bookMockito

У попередньому розділі ми пригадали, як писати юніт-тести. Проте в нашій програмі це не завжди зручно, оскільки тестування контролера вимагає тестування сервісу, а тестування сервісу — тестування репозиторію.

Цей ланцюг залежностей викликає питання: чи потрібно тестувати всі модулі та їхні залежності, чи можна імітувати їхню поведінку — іншими словами, замокати їх?

Mockito: Вступ

Mockito — це популярна бібліотека для Java, яка використовується для створення мок-об'єктів у юніт-тестах.

Вона дозволяє розробникам імітувати поведінку складних зовнішніх залежностей (таких як бази даних, сервіси або API), щоб ізолювати та перевірити логіку окремого компонента (зазвичай класу або методу) без взаємодії з цими зовнішніми системами.

Це робить юніт-тестування більш передбачуваним і швидким, оскільки немає потреби у реальних викликах зовнішніх ресурсів під час тестування.

Мок-об'єкти у Mockito

Це дозволяє розробникам переконатися, що компонент, який тестується (модуль), поводиться коректно без залежності від реальних залежностей, таких як бази даних або сторонні сервіси.

Клас може бути анотований за допомогою @Mock для створення його мок-версії. Ця анотація часто використовується разом із @InjectMocks для автоматичного впровадження залежностей у клас, що тестується.

@Mock 
private MyService myServiceMock;

Цей макетний об'єкт може бути ін'єктований як залежність в інший об'єкт за допомогою анотації @InjectMocks. Ця анотація автоматично ін'єктує макетні об'єкти у клас, який тестується.

@InjectMocks
private MyController myController;

Основні методи в Mockito

Mockito надає широкий спектр методів для керування макетними об'єктами, кожен з яких виконує певну функцію у процесі тестування. Нижче наведено найбільш важливі методи разом з їх поясненнями.

Тестування контролерів з Mockito

Тестування контролерів часто передбачає використання моків для сервісів, які викликаються всередині контролера. У цьому контексті Mockito відіграє ключову роль у ізоляції логіки контролера від сервісного шару.

Короткий зміст відео

Ми тестуємо контролери за допомогою класу MockMvc, але що він пропонує і які переваги його використання?

@Autowired
private MockMvc mockMvc;

За допомогою MockMvc можна імітувати різні HTTP-запити (GET, POST, PUT тощо), передавати параметри, заголовки та перевіряти відповіді, коди стану, заголовки й тіла відповідей, що значно спрощує модульне тестування контролерів.

Наприклад, можна використовувати методи на кшталт perform() для виконання запиту, andExpect() для перевірки очікуваних результатів і content() для перевірки вмісту відповіді.

Test.java

Test.java

copy
1234
mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())

Додатково, можна ланцюжити твердження для перевірки HTTP-статусу за допомогою status().isOk(), перевіряти структуру JSON-відповіді через jsonPath() та інше.

Test.java

Test.java

copy
1234
.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));

Ці інструменти забезпечують всебічне тестування поведінки контролера без реального виконання HTTP-запитів.

Ключові анотації

Анотація @WebMvcTest(BookController.class) використовується для тестування BookController, завантажуючи лише ті компоненти, які необхідні для цього конкретного контролера. Вона виключає решту інфраструктури Spring, дозволяючи зосередитися на тестуванні самого контролера.

@WebMvcTest(BookController.class)
public class BookControllerTest 

Додатково, анотація @MockBean, застосована до поля bookService, створює мок-версію цього сервісу, що дозволяє імітувати його поведінку. Це допомагає ізолювати контролер від його залежностей та зосередитися на тестуванні його логіки.

@MockBean
private BookService bookService;

Тести для методу updateBook

Ми написали два тести для методу updateBook, які охоплюють усі можливі випадки для цього методу. У першому тесті ми перевіряємо, що все пройшло успішно і що наша сутність була оновлена.

BookControllerTest.java

BookControllerTest.java

copy
1234567891011121314151617
@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }

Цей тест перевіряє успішне оновлення book, якщо він існує в базі даних. Викликається метод updateBook() сервісу з Id книги та об'єктом bookRequestDTO, після чого він повертає об'єкт bookResponseDTO, що представляє оновлену книгу.

Перевірка обробки виключень

Також є тест, у якому виникає виключення в методі updateBook, і необхідно перевірити, як контролер поводиться в такій ситуації.

BookControllerTest.java

BookControllerTest.java

copy
12345678910111213141516
@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }

Цей тест перевіряє поведінку контролера під час спроби оновити книгу, яка не знайдена у базі даних. Спочатку викликається метод updateBook() сервісу з Id книги та об'єктом bookRequestDTO.

Однак, замість успішного результату, сервіс генерує ApiException, що вказує на те, що книга з вказаним Id не знайдена.

Тестування сервісу з Mockito

Під час тестування сервісу важливо ізолювати його від залежностей, таких як репозиторії або зовнішні API. Mockito дозволяє створювати моки для цих залежностей та визначати їхню поведінку у тестах.

Короткий підсумок відео

Почнемо з тестування нашого методу у випадку, коли він успішно оновлює дані, тобто Id є валідним. Звісно, ми використаємо Mockito для імітації поведінки репозиторію.

BookServiceTest.java

BookServiceTest.java

copy
123456789101112131415161718192021222324252627282930313233
@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }

Метод bookRepository.findById("1") імітується для повернення існуючої Book з репозиторію, а bookRepository.save(bookWithRepository) імітується для повернення оновленої книги після збереження змін.

updateBook() метод сервісу викликається для оновлення книги на основі bookRequestDTO, повертаючи BookResponseDTO.

assertNotNull(result) гарантує, що результат не є null, що вказує на успішне оновлення, а assertEquals() перевіряє, що ID, name, author та price оновленої книги відповідають очікуваним значенням.

 assertNotNull(result);
 assertEquals("1", result.getId());
 assertEquals("Updated Name", result.getName());
 assertEquals("Updated Author", result.getAuthor());
 assertEquals("150", result.getPrice());

Нарешті, verify() переконується, що findById() та save() у репозиторії були викликані з правильними параметрами.

verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);

Перевірка винятку у сервісі

Цей тест перевіряє, що сервіс викидає ApiException, якщо Book, яку потрібно оновити, не знайдено у базі даних.

BookServiceTest.java

BookServiceTest.java

copy
1234567891011121314151617181920
@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }

Спочатку створюється об'єкт BookRequestDTO з даними для оновлення, і призначається тестовий ідентифікатор книги Id, який не існує в базі даних"999". Метод bookRepository.findById(idTest) мокиться для повернення Optional.empty(), що вказує на відсутність книги з таким Id.

when(bookRepository.findById(idTest)).thenReturn(Optional.empty());

Тест використовує метод assertThrows() для перевірки, що при виклику bookService.updateBook(idTest, bookRequestDTO) буде згенеровано ApiException.

Далі методи assertEquals() перевіряють, що повідомлення винятку відповідає очікуваному тексту, а статус помилки дорівнює HttpStatus.NOT_FOUND.

ApiException apiException = assertThrows(ApiException.class, () -> {
      bookService.updateBook(idTest, bookRequestDTO);
});

assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());

Метод verify(bookRepository, never()).save(any(Book.class)) гарантує, що метод збереження книги save() не викликався, оскільки книгу не знайдено і оновлення не виконано.

verify(bookRepository, never()).save(any(Book.class));

Підсумок

Mockito — це бібліотека для створення мок-об'єктів, яка дозволяє імітувати поведінку залежностей та ізолювати код, що тестується. Це спрощує тестування, робить його швидшим, стабільнішим і незалежним від зовнішніх ресурсів.

Все було зрозуміло?

Як ми можемо покращити це?

Дякуємо за ваш відгук!

Секція 5. Розділ 3

Запитати АІ

expand

Запитати АІ

ChatGPT

Запитайте про що завгодно або спробуйте одне із запропонованих запитань, щоб почати наш чат

Suggested prompts:

What are some common mistakes to avoid when using Mockito?

Can you explain the difference between @Mock and @MockBean?

How do I decide when to use mocks versus real objects in my tests?

Awesome!

Completion rate improved to 3.45

bookMockito

Свайпніть щоб показати меню

У попередньому розділі ми пригадали, як писати юніт-тести. Проте в нашій програмі це не завжди зручно, оскільки тестування контролера вимагає тестування сервісу, а тестування сервісу — тестування репозиторію.

Цей ланцюг залежностей викликає питання: чи потрібно тестувати всі модулі та їхні залежності, чи можна імітувати їхню поведінку — іншими словами, замокати їх?

Mockito: Вступ

Mockito — це популярна бібліотека для Java, яка використовується для створення мок-об'єктів у юніт-тестах.

Вона дозволяє розробникам імітувати поведінку складних зовнішніх залежностей (таких як бази даних, сервіси або API), щоб ізолювати та перевірити логіку окремого компонента (зазвичай класу або методу) без взаємодії з цими зовнішніми системами.

Це робить юніт-тестування більш передбачуваним і швидким, оскільки немає потреби у реальних викликах зовнішніх ресурсів під час тестування.

Мок-об'єкти у Mockito

Це дозволяє розробникам переконатися, що компонент, який тестується (модуль), поводиться коректно без залежності від реальних залежностей, таких як бази даних або сторонні сервіси.

Клас може бути анотований за допомогою @Mock для створення його мок-версії. Ця анотація часто використовується разом із @InjectMocks для автоматичного впровадження залежностей у клас, що тестується.

@Mock 
private MyService myServiceMock;

Цей макетний об'єкт може бути ін'єктований як залежність в інший об'єкт за допомогою анотації @InjectMocks. Ця анотація автоматично ін'єктує макетні об'єкти у клас, який тестується.

@InjectMocks
private MyController myController;

Основні методи в Mockito

Mockito надає широкий спектр методів для керування макетними об'єктами, кожен з яких виконує певну функцію у процесі тестування. Нижче наведено найбільш важливі методи разом з їх поясненнями.

Тестування контролерів з Mockito

Тестування контролерів часто передбачає використання моків для сервісів, які викликаються всередині контролера. У цьому контексті Mockito відіграє ключову роль у ізоляції логіки контролера від сервісного шару.

Короткий зміст відео

Ми тестуємо контролери за допомогою класу MockMvc, але що він пропонує і які переваги його використання?

@Autowired
private MockMvc mockMvc;

За допомогою MockMvc можна імітувати різні HTTP-запити (GET, POST, PUT тощо), передавати параметри, заголовки та перевіряти відповіді, коди стану, заголовки й тіла відповідей, що значно спрощує модульне тестування контролерів.

Наприклад, можна використовувати методи на кшталт perform() для виконання запиту, andExpect() для перевірки очікуваних результатів і content() для перевірки вмісту відповіді.

Test.java

Test.java

copy
1234
mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())

Додатково, можна ланцюжити твердження для перевірки HTTP-статусу за допомогою status().isOk(), перевіряти структуру JSON-відповіді через jsonPath() та інше.

Test.java

Test.java

copy
1234
.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));

Ці інструменти забезпечують всебічне тестування поведінки контролера без реального виконання HTTP-запитів.

Ключові анотації

Анотація @WebMvcTest(BookController.class) використовується для тестування BookController, завантажуючи лише ті компоненти, які необхідні для цього конкретного контролера. Вона виключає решту інфраструктури Spring, дозволяючи зосередитися на тестуванні самого контролера.

@WebMvcTest(BookController.class)
public class BookControllerTest 

Додатково, анотація @MockBean, застосована до поля bookService, створює мок-версію цього сервісу, що дозволяє імітувати його поведінку. Це допомагає ізолювати контролер від його залежностей та зосередитися на тестуванні його логіки.

@MockBean
private BookService bookService;

Тести для методу updateBook

Ми написали два тести для методу updateBook, які охоплюють усі можливі випадки для цього методу. У першому тесті ми перевіряємо, що все пройшло успішно і що наша сутність була оновлена.

BookControllerTest.java

BookControllerTest.java

copy
1234567891011121314151617
@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }

Цей тест перевіряє успішне оновлення book, якщо він існує в базі даних. Викликається метод updateBook() сервісу з Id книги та об'єктом bookRequestDTO, після чого він повертає об'єкт bookResponseDTO, що представляє оновлену книгу.

Перевірка обробки виключень

Також є тест, у якому виникає виключення в методі updateBook, і необхідно перевірити, як контролер поводиться в такій ситуації.

BookControllerTest.java

BookControllerTest.java

copy
12345678910111213141516
@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }

Цей тест перевіряє поведінку контролера під час спроби оновити книгу, яка не знайдена у базі даних. Спочатку викликається метод updateBook() сервісу з Id книги та об'єктом bookRequestDTO.

Однак, замість успішного результату, сервіс генерує ApiException, що вказує на те, що книга з вказаним Id не знайдена.

Тестування сервісу з Mockito

Під час тестування сервісу важливо ізолювати його від залежностей, таких як репозиторії або зовнішні API. Mockito дозволяє створювати моки для цих залежностей та визначати їхню поведінку у тестах.

Короткий підсумок відео

Почнемо з тестування нашого методу у випадку, коли він успішно оновлює дані, тобто Id є валідним. Звісно, ми використаємо Mockito для імітації поведінки репозиторію.

BookServiceTest.java

BookServiceTest.java

copy
123456789101112131415161718192021222324252627282930313233
@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }

Метод bookRepository.findById("1") імітується для повернення існуючої Book з репозиторію, а bookRepository.save(bookWithRepository) імітується для повернення оновленої книги після збереження змін.

updateBook() метод сервісу викликається для оновлення книги на основі bookRequestDTO, повертаючи BookResponseDTO.

assertNotNull(result) гарантує, що результат не є null, що вказує на успішне оновлення, а assertEquals() перевіряє, що ID, name, author та price оновленої книги відповідають очікуваним значенням.

 assertNotNull(result);
 assertEquals("1", result.getId());
 assertEquals("Updated Name", result.getName());
 assertEquals("Updated Author", result.getAuthor());
 assertEquals("150", result.getPrice());

Нарешті, verify() переконується, що findById() та save() у репозиторії були викликані з правильними параметрами.

verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);

Перевірка винятку у сервісі

Цей тест перевіряє, що сервіс викидає ApiException, якщо Book, яку потрібно оновити, не знайдено у базі даних.

BookServiceTest.java

BookServiceTest.java

copy
1234567891011121314151617181920
@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }

Спочатку створюється об'єкт BookRequestDTO з даними для оновлення, і призначається тестовий ідентифікатор книги Id, який не існує в базі даних"999". Метод bookRepository.findById(idTest) мокиться для повернення Optional.empty(), що вказує на відсутність книги з таким Id.

when(bookRepository.findById(idTest)).thenReturn(Optional.empty());

Тест використовує метод assertThrows() для перевірки, що при виклику bookService.updateBook(idTest, bookRequestDTO) буде згенеровано ApiException.

Далі методи assertEquals() перевіряють, що повідомлення винятку відповідає очікуваному тексту, а статус помилки дорівнює HttpStatus.NOT_FOUND.

ApiException apiException = assertThrows(ApiException.class, () -> {
      bookService.updateBook(idTest, bookRequestDTO);
});

assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());

Метод verify(bookRepository, never()).save(any(Book.class)) гарантує, що метод збереження книги save() не викликався, оскільки книгу не знайдено і оновлення не виконано.

verify(bookRepository, never()).save(any(Book.class));

Підсумок

Mockito — це бібліотека для створення мок-об'єктів, яка дозволяє імітувати поведінку залежностей та ізолювати код, що тестується. Це спрощує тестування, робить його швидшим, стабільнішим і незалежним від зовнішніх ресурсів.

Все було зрозуміло?

Як ми можемо покращити це?

Дякуємо за ваш відгук!

Секція 5. Розділ 3
some-alt