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

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

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