Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Каким должен быть хороший Unit тест
Хороший unit тест — это быстрый, независимый, чистый и понятный тест, который проверяет одно и только одно поведение. Есть конкретные правила.
Характеристики хорошего unit теста
1. FAST (Быстрый)
Unit тесты должны выполняться в миллисекундах, не в секундах.
// ❌ ПЛОХО: медленный тест
@Test
public void testCalculation() throws Exception {
Thread.sleep(1000); // Спит 1 секунду!
UserService service = new UserService(new RealDatabase());
service.getUser(1L); // Обращается в реальную БД
// ...
}
// ✅ ХОРОШО: быстрый тест
@Test
public void testCalculation() {
UserRepository mockRepo = Mockito.mock(UserRepository.class);
Mockito.when(mockRepo.findById(1L)).thenReturn(testUser);
UserService service = new UserService(mockRepo);
User result = service.getUser(1L);
assertThat(result).isEqualTo(testUser);
// Выполняется в миллисекундах
}
Правило: Unit тест должен выполняться < 100ms.
2. ISOLATED (Независимый)
Тест не должен зависеть от других тестов, БД, API, файловой системы.
// ❌ ПЛОХО: зависит от БД
@Test
public void testRegisterUser() {
// Данные могут измениться, если другой тест их изменил
User user = database.findUserByEmail("test@example.com");
assertThat(user).isNotNull();
}
// ✅ ХОРОШО: независимый
@Test
public void testRegisterUser() {
UserRepository mockRepo = Mockito.mock(UserRepository.class);
User testUser = new User("John", "john@example.com");
Mockito.when(mockRepo.findByEmail("john@example.com")).thenReturn(testUser);
UserService service = new UserService(mockRepo);
User result = service.getUser("john@example.com");
assertThat(result).isEqualTo(testUser);
// Результат одинаковый независимо от других тестов
}
3. REPEATABLE (Повторяемый)
Тест должен давать одинаковый результат каждый раз, не важно сколько раз его запускать.
// ❌ ПЛОХО: непредсказуемый результат
@Test
public void testGetCurrentTime() {
LocalDateTime now = LocalDateTime.now();
assertEquals(now, service.getTime()); // Не совпадёт никогда!
}
// ✅ ХОРОШО: детерминированный
@Test
public void testGetCurrentTime() {
LocalDateTime fixedTime = LocalDateTime.of(2024, 1, 1, 12, 0, 0);
TimeService service = new TimeService(() -> fixedTime); // Injection
assertEquals(fixedTime, service.getTime());
}
4. SELF-CHECKING (Само-проверяющийся)
Тест сам проверяет результат, не нужна ручная проверка.
// ❌ ПЛОХО: требует ручной проверки
@Test
public void testCalculation() {
int result = calculate(5, 3);
System.out.println(result); // Смотри консоль!
}
// ✅ ХОРОШО: автоматическая проверка
@Test
public void testCalculation() {
int result = calculate(5, 3);
assertEquals(8, result);
// Тест пройдёт или упадёт, не нужна ручная проверка
}
5. TIMELY (Вовремя написанный)
Тесты должны быть написаны перед или вместе с кодом, не после.
❌ ПЛОХО: сначала код, потом когда-нибудь тесты
1. Написал функцию
2. Забыл про тесты
3. Месяц спустя: "надо бы тесты..."
✅ ХОРОШО: TDD подход
1. Пишу падающий тест (RED)
2. Пишу минимальный код (GREEN)
3. Рефакторю (REFACTOR)
Struktura хорошего теста: AAA (Arrange-Act-Assert)
public class CalculatorTest {
@Test
public void testAddition() {
// ARRANGE (подготовка)
Calculator calc = new Calculator();
int a = 5;
int b = 3;
int expectedResult = 8;
// ACT (действие)
int actualResult = calc.add(a, b);
// ASSERT (проверка)
assertEquals(expectedResult, actualResult);
}
}
Правила именования хорошего unit теста
// ✅ ХОРОШО: тестMethodName_Condition_ExpectedResult
@Test
public void testGetUser_UserExists_ReturnsUser() { }
@Test
public void testGetUser_UserNotFound_ReturnsNull() { }
@Test
public void testRegister_InvalidEmail_ThrowsException() { }
@Test
public void testCalculate_DivideByZero_ThrowsArithmeticException() { }
// ИЛИ: shouldDoXWhenYZ (BDD стиль)
@Test
public void shouldReturnUserWhenUserExists() { }
@Test
public void shouldThrowExceptionWhenEmailIsInvalid() { }
Пример хорошего unit теста
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testRegisterUser_ValidEmail_SavesUser() {
// ARRANGE
String email = "john@example.com";
String password = "password123";
User expectedUser = new User(email, password);
Mockito.when(userRepository.save(Mockito.any(User.class)))
.thenReturn(expectedUser);
// ACT
User result = userService.registerUser(email, password);
// ASSERT
assertThat(result)
.isNotNull()
.hasFieldOrPropertyWithValue("email", email);
Mockito.verify(userRepository, Mockito.times(1))
.save(Mockito.any(User.class));
}
@Test
public void testRegisterUser_InvalidEmail_ThrowsException() {
// ARRANGE
String invalidEmail = "invalid-email";
// ACT & ASSERT
assertThatThrownBy(
() -> userService.registerUser(invalidEmail, "password")
)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid email");
}
@Test
public void testGetUser_UserNotFound_ReturnsNull() {
// ARRANGE
Mockito.when(userRepository.findById(999L))
.thenReturn(null);
// ACT
User result = userService.getUser(999L);
// ASSERT
assertThat(result).isNull();
}
}
Что НЕ должно быть в unit тестах
❌ Обращение к БД: используй mocks ❌ HTTP запросы: используй WireMock или mock ❌ Файловая система: используй temp files или mock ❌ Thread.sleep(): не нужно спать ❌ Зависимости от других тестов: каждый тест независим ❌ Случайные данные: используй фиксированные значения ❌ Комментарии вместо понятного кода: имена должны быть понятны ❌ Много утверждений: один assert или логически связанные ❌ Приватные методы: не тестируй приватные, тестируй public API
One Assertion per Test (или логически связанные)
// ❌ ПЛОХО: много проверок
@Test
public void testUser() {
User user = createUser();
assertEquals("John", user.getName());
assertEquals(30, user.getAge());
assertEquals("john@example.com", user.getEmail());
assertTrue(user.isActive());
assertFalse(user.isBlocked());
// ...
}
// ✅ ХОРОШО: отдельные тесты
@Test
public void testUserName() {
User user = createUser();
assertEquals("John", user.getName());
}
@Test
public void testUserEmail() {
User user = createUser();
assertEquals("john@example.com", user.getEmail());
}
// ИЛИ логически связанные проверки одного компонента
@Test
public void testUserCreation() {
User user = new User("John", "john@example.com");
assertThat(user)
.hasFieldOrPropertyWithValue("name", "John")
.hasFieldOrPropertyWithValue("email", "john@example.com")
.hasFieldOrPropertyWithValue("active", true);
}
Проверка исключений
// ❌ ПЛОХО: проверяет, что исключение выбросилось
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
userService.register(""); // Просто ждём исключение
}
// ✅ ХОРОШО: проверяет тип и сообщение
@Test
public void testInvalidInput() {
assertThatThrownBy(
() -> userService.register("")
)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Email cannot be empty");
}
Мифы о unit тестах
| Миф | Правда |
|---|---|
| Unit тесты = QA | Нет, это инструмент разработчика |
| 100% coverage = хорошо | Нет, 80-90% лучше (есть dead code) |
| Один assert = священный закон | Нет, логически связанные ok |
| Unit тесты медленные | Если slow — это не unit тесты |
| Не нужны, если код прост | Даже простой код может иметь баги |
Итог
Хороший unit тест:
- FAST — выполняется в миллисекундах
- ISOLATED — не зависит от других тестов/ресурсов
- REPEATABLE — одинаковый результат каждый раз
- SELF-CHECKING — сам проверяет результат
- TIMELY — написан до или с кодом (TDD)
- Понятное имя — описывает, что тестируется
- AAA структура — Arrange-Act-Assert
- Использует моки — для изоляции от зависимостей
- Один фокус — тестирует одно поведение
- Не имеет побочных эффектов — не меняет состояние
Эти правила гарантируют, что тесты будут полезны и не станут обузой при рефакторинге.