Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Покрываешь ли код Unit тестами
Ответ: ДА, абсолютно. Unit тесты — это фундамент качественной разработки. Без них невозможно писать maintainable код.
Почему Unit тесты критичны
1. Уверенность в коде
Kогда у тебя есть хорошие тесты:
- ✅ Можешь рефакторить без страха что-то сломать
- ✅ Видишь регрессии сразу при коммите
- ✅ Новые разработчики могут понять код через тесты
2. Документация
Тесты показывают, как использовать класс:
// Тест = документация
@Test
public void shouldReturnUserWhenIdExists() {
// User service должно вернуть пользователя по ID
UserService service = new UserService();
User user = service.getUserById(123L);
assertThat(user).isNotNull();
assertThat(user.getId()).isEqualTo(123L);
}
3. Раннее обнаружение ошибок
Тесты ловят ошибки во время разработки, а не на продакшене.
Структура Unit тестов в Java
1. Фреймворки
- JUnit 5 (JUnit Jupiter) — стандарт
- TestNG — альтернатива
- Spock — для groovy (более выразительно)
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class UserServiceTest {
@Test
public void testGetUserById() {
// Arrange
UserService service = new UserService();
// Act
User user = service.getUserById(1L);
// Assert
assertThat(user.getName()).isEqualTo("John");
}
}
2. Библиотеки для assertions
-
AssertJ — очень читаемо
assertThat(user).isNotNull().hasFieldOrPropertyWithValue("age", 25); -
Hamcrest — стиль matcher'ов
assertThat(user, hasProperty("name", equalTo("John"))); -
Junit assertions — встроенные
assertEquals("John", user.getName());
3. Моки (Mockito)
Для изоляции тестируемого кода от зависимостей:
import org.mockito.Mock;
import org.mockito.InjectMocks;
public class OrderServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private OrderService orderService;
@Test
public void shouldCreateOrder() {
// Mock возвращает fake пользователя
User mockUser = new User(1L, "John");
when(userRepository.findById(1L)).thenReturn(mockUser);
// Тестируем OrderService в изоляции
Order order = orderService.createOrder(1L, "Product");
assertThat(order.getUserId()).isEqualTo(1L);
verify(userRepository).findById(1L); // Проверяем, что был вызван
}
}
AAA паттерн (Arrange, Act, Assert)
@Test
public void calculateDiscount() {
// Arrange: подготавливаем данные
Product product = new Product("Laptop", 1000.0);
Customer customer = new Customer("VIP");
PricingService pricingService = new PricingService();
// Act: вызываем функцию
double discountedPrice = pricingService.calculatePrice(product, customer);
// Assert: проверяем результат
assertThat(discountedPrice).isEqualTo(900.0); // VIP скидка 10%
}
Типы Unit тестов
1. Тесты успешного пути (Happy Path)
@Test
public void shouldValidateEmail() {
EmailValidator validator = new EmailValidator();
boolean isValid = validator.validate("user@example.com");
assertThat(isValid).isTrue();
}
2. Тесты граничных случаев (Edge Cases)
@Test
public void shouldHandleEmptyString() {
EmailValidator validator = new EmailValidator();
boolean isValid = validator.validate("");
assertThat(isValid).isFalse();
}
@Test
public void shouldHandleNullInput() {
EmailValidator validator = new EmailValidator();
assertThrows(NullPointerException.class, () -> {
validator.validate(null);
});
}
3. Тесты исключений (Exception Testing)
@Test
public void shouldThrowExceptionForInvalidId() {
UserService service = new UserService();
assertThrows(IllegalArgumentException.class, () -> {
service.getUserById(-1L);
});
}
@Test
public void shouldThrowWithCorrectMessage() {
UserService service = new UserService();
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
service.getUserById(-1L);
});
assertThat(exception.getMessage()).contains("ID must be positive");
}
Параметризованные тесты (Parameterized Tests)
@ParameterizedTest
@ValueSource(strings = { "user@gmail.com", "admin@example.org", "test@domain.io" })
public void shouldValidateMultipleEmails(String email) {
EmailValidator validator = new EmailValidator();
assertThat(validator.validate(email)).isTrue();
}
@ParameterizedTest
@CsvSource({
"user@gmail.com, true",
"invalid-email, false",
"another@test.com, true"
})
public void testEmailWithData(String email, boolean expected) {
EmailValidator validator = new EmailValidator();
assertThat(validator.validate(email)).isEqualTo(expected);
}
Coverage рекомендации
Хороший покрытие:
- Бизнес-логика: 80-90% coverage
- Контроллеры: 70-80% coverage
- Конфигурация: 0-10% coverage (не нужно тестировать Spring configs)
- Getters/Setters: 0-50% coverage (нет смысла)
Проверка coverage:
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<configuration>
<rules>
<rule>
<element>PACKAGE</element>
<excludes>
<exclude>*Test</exclude>
</excludes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
Запуск coverage:
mvn clean test jacoco:report
# Отчёт в target/site/jacoco/index.html
Реальный пример: User Service
public class UserService {
private UserRepository userRepository;
private EmailValidator emailValidator;
public UserService(UserRepository repo, EmailValidator validator) {
this.userRepository = repo;
this.emailValidator = validator;
}
public User createUser(String email, String name) {
if (!emailValidator.validate(email)) {
throw new IllegalArgumentException("Invalid email");
}
if (userRepository.findByEmail(email) != null) {
throw new IllegalArgumentException("User already exists");
}
User user = new User(email, name);
return userRepository.save(user);
}
}
Тесты для UserService:
public class UserServiceTest {
private UserService userService;
private UserRepository userRepository;
private EmailValidator emailValidator;
@BeforeEach
public void setUp() {
userRepository = mock(UserRepository.class);
emailValidator = mock(EmailValidator.class);
userService = new UserService(userRepository, emailValidator);
}
// Happy path
@Test
public void shouldCreateUserSuccessfully() {
when(emailValidator.validate("john@example.com")).thenReturn(true);
when(userRepository.findByEmail("john@example.com")).thenReturn(null);
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
User user = userService.createUser("john@example.com", "John");
assertThat(user.getEmail()).isEqualTo("john@example.com");
verify(userRepository).save(any());
}
// Invalid email
@Test
public void shouldThrowForInvalidEmail() {
when(emailValidator.validate("invalid")).thenReturn(false);
assertThrows(IllegalArgumentException.class, () -> {
userService.createUser("invalid", "John");
});
}
// User already exists
@Test
public void shouldThrowIfUserAlreadyExists() {
when(emailValidator.validate("john@example.com")).thenReturn(true);
when(userRepository.findByEmail("john@example.com")).thenReturn(new User());
assertThrows(IllegalArgumentException.class, () -> {
userService.createUser("john@example.com", "John");
});
}
}
Best Practices для Unit тестов
✅ Делай:
- Один assert (или логически связанные) в тесте
- Дай описательные имена:
shouldReturnUserWhenIdExists - Изолируй тесты (no shared state)
- Используй fixtures и builders
- Тесты должны быть быстрыми (< 100ms)
❌ Не делай:
- Тесты, которые зависят друг от друга
- Sleep() в тестах
- Тестирование приватных методов (они должны тестироваться через public)
- Mock'ирование всего подряд
- Огромные arrange блоки (значит, класс сложный)
Вывод
ДА, я покрываю код Unit тестами, потому что:
- ✅ Гарантия качества
- ✅ Документация для других разработчиков
- ✅ Раннее обнаружение ошибок
- ✅ Уверенность при рефакторинге
- ✅ Снижение затрат на отладку в production