← Назад к вопросам
Нужно ли править тест, если реализация не проходит тест при корректном поведении?
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
│ └─ Измени тест И реализацию
│
└─ Ошибка в тесте?
├─ ДА → Логическая ошибка
│ └─ Измени тест
│
└─ Тест проверяет реализацию?
├─ ДА → Переделай тест на поведение
│
└─ Всё остальное?
└─ Исправляй реализацию!
Выводы
- Тест = Требование (Specification)
- Если тест падает → Реализация неправильная (99% случаев)
- Меняй реализацию, не тест (в подавляющем большинстве)
- Меняй тест ТОЛЬКО если:
- Требование официально изменилось
- Тест имеет логическую ошибку
- Тест проверяет реализацию вместо поведения
- TDD дисциплина: Тест первым, реализация второй
- Тесты — защита от регрессии, их нельзя игнорировать