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

Нужно ли править тест, если реализация не проходит тест при корректном поведении?

2.3 Middle🔥 71 комментариев
#Тестирование

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

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

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

Исправление тестов при корректной реализации: TDD принципы

Отличный вопрос о TDD (Test-Driven Development) и вопрос философии тестирования! Ответ: НЕТ, в подавляющем большинстве случаев НЕ нужно. Позвольте разобраться в деталях.

Краткий ответ: Сначала переосмотри реализацию

Правило TDD:

Тест = Specification (требование)
Реализация = Implementation

Если реализация не проходит тест → Реализация неправильная
Если тест неправильный → Это отдельный случай

Когда НЕЛЬЗЯ исправлять тест

99% случаев: тест — это требование

// Пример: Тест требует, что все имена должны начинаться с заглавной буквы
@Test
public void testNameValidation() {
    User user = new User();
    
    // Требование: имя должно начинаться с заглавной буквы
    assertThrows(IllegalArgumentException.class, () -> {
        user.setName("john");  // ❌ Маленькая буква
    });
    
    // Это должно пройти
    user.setName("John");  // ✅ Большая буква
}

// Реализация ДО исправления:
public class User {
    public void setName(String name) {
        // Забыли добавить валидацию!
        this.name = name;
    }
}

// ❌ НЕПРАВИЛЬНО: Менять тест, чтобы пройти!
@Test
public void testNameValidation() {
    User user = new User();
    
    // Убрали требование
    user.setName("john");  // Теперь работает
}

// ✅ ПРАВИЛЬНО: Исправить реализацию!
public class User {
    public void setName(String name) {
        if (!Character.isUpperCase(name.charAt(0))) {
            throw new IllegalArgumentException("Name must start with uppercase");
        }
        this.name = name;
    }
}

Когда МОЖНО исправлять тест (редкие случаи)

Случай 1: Неправильное требование (BDD/Product decision)

// Изначальное требование было ошибочным
@Test
public void testPriceCalculation() {
    Order order = new Order();
    order.addItem(new Item(100));
    order.addItem(new Item(100));
    
    // Старое требование: сумма всегда округляется вверх
    assertEquals(201, order.getTotal());  // ❌ Неправильно
}

// Product решил: нужно использовать банковское округление
// Тогда переосмотр происходит вместе с Product Owner

@Test
public void testPriceCalculation() {
    Order order = new Order();
    order.addItem(new Item(100));
    order.addItem(new Item(100));
    
    // Новое требование: банковское округление (Half Even)
    assertEquals(200, order.getTotal());  // ✅ Правильно
}

Случай 2: Тест был написан неправильно (ошибка в тестовой логике)

// ❌ НЕПРАВИЛЬНЫЙ ТЕСТ (ошибка в самом тесте)
@Test
public void testListSort() {
    List<Integer> list = Arrays.asList(3, 1, 2);
    list.sort(Comparator.naturalOrder());
    
    // Забыли про immutable List.sort()!
    // List.sort() работает в-место (in-place), не возвращает новый список
    // AssertJ.assertThat(list)  // list == [3, 1, 2]
    // Это НЕПРАВИЛЬНО!
    
    assertEquals(Arrays.asList(1, 2, 3), list);
}

// Это тест имеет ошибку в логике, не реализация
// Правильный тест:
@Test
public void testListSort() {
    List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
    list.sort(Comparator.naturalOrder());
    
    // Теперь список изменился in-place
    assertEquals(Arrays.asList(1, 2, 3), list);
}

Случай 3: Тест проверяет реализационный деталь, а не поведение

// ❌ ПЛОХОЙ ТЕСТ (проверяет реализацию, а не требование)
@Test
public void testCacheImplementation() {
    CacheService cache = new CacheService();
    
    // Проверяем реализацию (что используется HashMap)
    Field field = cache.getClass().getDeclaredField("data");
    field.setAccessible(true);
    HashMap map = (HashMap) field.get(cache);
    
    // Это плохой тест! Завязана на реализацию
    assertEquals(HashMap.class, map.getClass());
}

// ✅ ПРАВИЛЬНЫЙ ТЕСТ (проверяет поведение)
@Test
public void testCachePutAndGet() {
    CacheService cache = new CacheService();
    
    cache.put("key", "value");
    assertEquals("value", cache.get("key"));
    
    // Мне всё равно HashMap или ConcurrentHashMap
    // Мне важно поведение
}

Примеры правильного TDD процесса

Пример 1: Разработка UserValidator

// ШАГ 1: Написать тест (RED)
@Test
public void testValidateEmail() {
    UserValidator validator = new UserValidator();
    
    // Требование: email должен быть валидным
    assertThrows(IllegalArgumentException.class, () -> {
        validator.validate(new User("John", "invalid-email"));
    });
}

// ШАГ 2: Написать минимальную реализацию (GREEN)
public class UserValidator {
    public void validate(User user) {
        if (!user.getEmail().contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

// ШАГ 3: Рефакторить (REFACTOR)
// Тест не менять! Только улучшать реализацию
public class UserValidator {
    private static final String EMAIL_REGEX = 
        "^[A-Za-z0-9+_.-]+@(.+)$";
    
    public void validate(User user) {
        if (!Pattern.matches(EMAIL_REGEX, user.getEmail())) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

// Тест НЕ МЕНЯЕТСЯ! Только реализация улучшается

Пример 2: Payment Processing

// ТРЕБОВАНИЕ 1: Успешный платёж
@Test
public void testSuccessfulPayment() {
    PaymentProcessor processor = new PaymentProcessor();
    
    PaymentResult result = processor.process(new PaymentRequest(
        100.0, "CARD", "4111111111111111"
    ));
    
    assertTrue(result.isSuccess());
}

// ТРЕБОВАНИЕ 2: Отклонённый платёж (invalid card)
@Test
public void testFailedPaymentInvalidCard() {
    PaymentProcessor processor = new PaymentProcessor();
    
    PaymentResult result = processor.process(new PaymentRequest(
        100.0, "CARD", "1234567890123456"
    ));
    
    assertFalse(result.isSuccess());
    assertEquals("Invalid card", result.getErrorMessage());
}

// Эти тесты НИКОГДА не меняются
// Они — требования!
// Если реализация не проходит — исправляй реализацию

Когда я меняю тест (личный опыт)

Я меняю тест в 1% случаев:

1. Product Owner решил менять требование (с документацией)
2. Тест содержит ошибку в самой логике теста
3. Тест проверял реализацию, а не поведение
4. Требование физически невозможно (например, NaN в тесте)

Все остальные случаи → исправляю реализацию

Правило: Green -> Refactor

// RED: Тест падает
@Test
public void testComplexCalculation() {
    Calculator calc = new Calculator();
    assertEquals(7, calc.add(3, 4));
}

// GREEN: Минимальная реализация
public class Calculator {
    public int add(int a, int b) {
        return 7;  // Hardcode!
    }
}

// REFACTOR: Улучшаем, но тест не меняем
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// Тест остался тем же!

Антипаттерны: Когда разработчики неправильно меняют тесты

// ❌ АНТИПАТТЕРН 1: Skip falling test
@Test
@Disabled  // Пропускаем вместо того, чтобы исправить
public void testImportantFeature() {
    // Это плохо! Тест показывает проблему
}

// ❌ АНТИПАТТЕРН 2: Ослабить требование
@Test
public void testPerformance() {
    long start = System.nanoTime();
    performHeavyOperation();
    long elapsed = System.nanoTime() - start;
    
    // Изначально: 100ms
    // Теперь: 10000ms (потому что медленно)
    assertTrue(elapsed < 10_000_000_000);  // ❌ Слишком слабо
}

// ✅ ПРАВИЛЬНО: Исправить реализацию
// Оптимизировать код, используя кэширование, индексы и т.д.

// ❌ АНТИПАТТЕРН 3: Удалить неудачный тест
delete testEdgeCaseWithNegativeNumbers();
// Это скрывает баг!

// ✅ ПРАВИЛЬНО: Исправить реализацию, чтобы обработать edge case

Decision Tree: Менять ли тест?

Тест падает?
├─ ДА → Реализация неправильная
│  └─ Исправляй реализацию!
│
└─ Требование изменилось?
   ├─ ДА → С одобрения Product Owner
   │  └─ Измени тест И реализацию
   │
   └─ Ошибка в тесте?
      ├─ ДА → Логическая ошибка
      │  └─ Измени тест
      │
      └─ Тест проверяет реализацию?
         ├─ ДА → Переделай тест на поведение
         │
         └─ Всё остальное?
            └─ Исправляй реализацию!

Выводы

  1. Тест = Требование (Specification)
  2. Если тест падает → Реализация неправильная (99% случаев)
  3. Меняй реализацию, не тест (в подавляющем большинстве)
  4. Меняй тест ТОЛЬКО если:
    • Требование официально изменилось
    • Тест имеет логическую ошибку
    • Тест проверяет реализацию вместо поведения
  5. TDD дисциплина: Тест первым, реализация второй
  6. Тесты — защита от регрессии, их нельзя игнорировать