У цій лекції зосередимося на практичному аспекті: написанні unit-тестів для сервісів. Ми створимо тести для ключових компонентів бізнес-логіки одного з мікросервісів, використовуючи вже знайомі інструменти.
Підготовка до написання Unit-тестів
Unit-тести — це база тестування під час розробки. Вони перевіряють невеликі шматки коду, ізольовані від зовнішніх залежностей. Наприклад, ти хочеш протестувати метод сервісу, який рахує знижку, без звернень до бази даних, API іншого сервісу або сторонніх бібліотек.
Цілі:
- Ізолювати тестований код.
- Перевірити коректність бізнес-логіки.
- Переконатися, що граничні умови та виняткові ситуації оброблені правильно.
Оточення для тестування
На початку переконайся, що проєкт налаштований для тестування. Перевір, що в тебе є наступні залежності:
pom.xml для Maven:
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
build.gradle для Gradle:
dependencies {
// JUnit 5
testImplementation 'org.junit.jupiter:junit-jupiter'
// Mockito
testImplementation 'org.mockito:mockito-core'
// Spring Boot Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Приклад застосунку: магазин знижок
Візьмемо як приклад мікросервіс для обробки знижок. У нас є сервіс DiscountService, який обчислює розмір знижки на основі категорії користувача та кількості купленого товару.
Ось сам сервіс:
import org.springframework.stereotype.Service;
@Service
public class DiscountService {
public double calculateDiscount(String userCategory, int itemCount) {
if (itemCount <= 0) {
throw new IllegalArgumentException("Item count must be greater than 0");
}
switch (userCategory.toLowerCase()) {
case "vip":
return itemCount * 0.2; // 20% знижка
case "regular":
return itemCount > 5 ? itemCount * 0.1 : 0.0; // 10% знижка, якщо покупок > 5
default:
return 0.0; // Без знижки
}
}
}
Крок 1: Налаштування тестового класу
За угодами створимо тестовий клас DiscountServiceTest. Переконайся, що він лежить в директорії src/test/java.
package com.example.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Тести для DiscountService.
*/
public class DiscountServiceTest {
private final DiscountService discountService = new DiscountService();
@Test
void testCalculateDiscountForVipUser() {
double discount = discountService.calculateDiscount("VIP", 10);
assertEquals(2.0, discount, "VIP user should get 20% discount");
}
@Test
void testCalculateDiscountForRegularUser() {
double discount = discountService.calculateDiscount("regular", 6);
assertEquals(0.6, discount, "Regular user should get 10% discount for more than 5 items");
}
@Test
void testCalculateDiscountForUnknownUser() {
double discount = discountService.calculateDiscount("guest", 5);
assertEquals(0.0, discount, "Guest users should not get a discount");
}
@Test
void testCalculateDiscountWithZeroItems() {
assertThrows(IllegalArgumentException.class, () ->
discountService.calculateDiscount("VIP", 0),
"Should throw exception when item count is zero or less"
);
}
}
Обговорення тестів
- Позитивні сценарії: тести перевіряють коректний результат для "VIP" і "regular" користувачів.
- Негативні сценарії: тест з "guest" категорією і тест з нульовою кількістю товарів.
- Винятки: перевірка, що для недопустимих параметрів кидається виняток
IllegalArgumentException.
Крок 2: Додавання взаємодії з Mock (Mockito)
Тепер припустімо, що DiscountService залежить від іншого сервісу, наприклад, UserRepository, який повертає інформацію про користувача. Ми можемо використати Mockito для створення мока.
Впровадимо залежність:
@Service
public class DiscountService {
private final UserRepository userRepository;
public DiscountService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public double calculateDiscount(String userId, int itemCount) {
if (itemCount <= 0) {
throw new IllegalArgumentException("Item count must be greater than 0");
}
String userCategory = userRepository.getUserCategory(userId);
switch (userCategory.toLowerCase()) {
case "vip":
return itemCount * 0.2;
case "regular":
return itemCount > 5 ? itemCount * 0.1 : 0.0;
default:
return 0.0;
}
}
}
Тестуємо з Mockito:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class DiscountServiceTest {
private UserRepository userRepository;
private DiscountService discountService;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
discountService = new DiscountService(userRepository);
}
@Test
void testCalculateDiscountWithMockedUser() {
when(userRepository.getUserCategory("123")).thenReturn("VIP");
double discount = discountService.calculateDiscount("123", 10);
assertEquals(2.0, discount);
verify(userRepository, times(1)).getUserCategory("123");
}
@Test
void testCalculateDiscountForUnknownUser() {
when(userRepository.getUserCategory("456")).thenReturn("guest");
double discount = discountService.calculateDiscount("456", 5);
assertEquals(0.0, discount);
verify(userRepository, times(1)).getUserCategory(anyString());
}
}
- Створення мока: ми створюємо мок для
UserRepository— фейкової реалізації, яка не прив'язана до реальної бази даних. - Stub методів: використовуємо метод
when().thenReturn()для вказівки повертаного значення. - Перевірка викликів:
verify()перевіряє, що метод був викликаний на мокові з вказаними параметрами.
Типові помилки та їх виправлення
- Неправильне мокування: якщо забудеш вказати
when().thenReturn(), мок буде повертатиnullза замовчуванням. - Не використання
verify: перевірка викликів допомагає переконатися, що тестований метод працює з моками коректно. - Неопрацьовані винятки: якщо не перевіряти винятки, тести можуть пропустити критичні помилки.
Практичне застосування
Навички написання unit-тестів корисні для будь-якої backend-розробки. На співбесідах часто питають вміння мокати залежності і писати тести для складної бізнес-логіки. У реальних проєктах це допомагає пришвидшити розробку, бо помилки виявляються на ранніх етапах.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ