← Назад к вопросам

Каким должен быть хороший Unit тест?

1.0 Junior🔥 161 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Каким должен быть хороший 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 тест:

  1. FAST — выполняется в миллисекундах
  2. ISOLATED — не зависит от других тестов/ресурсов
  3. REPEATABLE — одинаковый результат каждый раз
  4. SELF-CHECKING — сам проверяет результат
  5. TIMELY — написан до или с кодом (TDD)
  6. Понятное имя — описывает, что тестируется
  7. AAA структура — Arrange-Act-Assert
  8. Использует моки — для изоляции от зависимостей
  9. Один фокус — тестирует одно поведение
  10. Не имеет побочных эффектов — не меняет состояние

Эти правила гарантируют, что тесты будут полезны и не станут обузой при рефакторинге.