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

Является ли код корректным, если до и после изменения тест завершался успешно?

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

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

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

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

Корректность кода и успешные тесты

Ответ: НЕТ, зелёные тесты не гарантируют корректность кода. Это распространенное заблуждение, которое может привести к серьезным багам в production.

Почему зелёные тесты могут быть обманчивы?

1. Некорректные или неполные тесты

Тесты могут быть написаны неправильно и проверять не то, что нужно.

// Пример: неправильный тест
@Test
public void testCalculateDiscount() {
    PricingService service = new PricingService();
    double result = service.calculateDiscount(100);
    
    // Плохой тест — не проверяет конкретное значение
    assertTrue(result > 0);  // ❌ Пройдёт при любом положительном числе
    
    // Правильный тест
    assertEquals(10.0, result, 0.01);  // ✓ Проверяет точное значение
}

2. Граничные случаи не покрыты

public class PaymentProcessor {
    public boolean processPayment(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) > 0) {
            // Логика обработки платежа
            return true;
        }
        return false;
    }
}

@Test
public void testPaymentProcessing() {
    PaymentProcessor processor = new PaymentProcessor();
    assertTrue(processor.processPayment(new BigDecimal("100.00")));
    // ✓ Тест проходит
}

// Но что с граничными случаями?
// - Отрицательная сумма?
// - Ноль?
// - Очень большие числа?
// - Null значение?
// Если нет тестов для этих случаев — код может упасть в production!

3. Тесты зависят от состояния

public class UserRepository {
    private static List<User> users = new ArrayList<>();  // ❌ Статический state
    
    public void saveUser(User user) {
        users.add(user);
    }
    
    public int getUserCount() {
        return users.size();
    }
}

@Test
public void testUserCount() {
    UserRepository repo = new UserRepository();
    repo.saveUser(new User("Alice"));
    assertEquals(1, repo.getUserCount());
    // ✓ Тест проходит в первый раз
    
    // Но если запустить дважды подряд, второй раз users.size() = 2!
    // Тесты зависят друг от друга и от порядка выполнения
}

Решение: Использовать setup/teardown для очистки состояния.

4. Race conditions и многопоточность

public class ThreadSafeCounter {
    private int count = 0;  // ❌ Не потокобезопасно!
    
    public void increment() {
        count++;  // ❌ Race condition
    }
    
    public int getCount() {
        return count;
    }
}

@Test
public void testIncrement() {
    ThreadSafeCounter counter = new ThreadSafeCounter();
    for (int i = 0; i < 100; i++) {
        counter.increment();
    }
    assertEquals(100, counter.getCount());
    // ✓ Может пройти (если не будет race condition)
}

@Test
public void testConcurrentIncrement() throws InterruptedException {
    ThreadSafeCounter counter = new ThreadSafeCounter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) counter.increment();
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 1000; i++) counter.increment();
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    
    // ❌ Может быть 1952 вместо 2000 из-за race condition!
    assertEquals(2000, counter.getCount());
}

5. Моки скрывают реальные проблемы

public class UserService {
    private DatabaseConnection db;
    
    public UserService(DatabaseConnection db) {
        this.db = db;
    }
    
    public User findUserById(int id) {
        return db.query("SELECT * FROM users WHERE id = " + id);
        // ❌ SQL injection!
    }
}

@Test
public void testFindUser() {
    // Используем mock вместо реальной БД
    DatabaseConnection mockDb = mock(DatabaseConnection.class);
    User user = new User(1, "Alice");
    when(mockDb.query(any())).thenReturn(user);
    
    UserService service = new UserService(mockDb);
    User result = service.findUserById(1);
    
    assertEquals("Alice", result.getName());
    // ✓ Тест проходит, но SQL injection не обнаружена!
}

// Правильное решение: использовать параметризованные запросы
public User findUserById(int id) {
    return db.query("SELECT * FROM users WHERE id = ?", id);
    // ✓ Защищено от SQL injection
}

6. Производительность и deadlock

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void method1() {
        synchronized(lock1) {
            Thread.sleep(10);
            synchronized(lock2) {
                // Операция
            }
        }
    }
    
    public void method2() {
        synchronized(lock2) {  // Обратный порядок блокировок!
            Thread.sleep(10);
            synchronized(lock1) {
                // Операция
            }
        }
    }
}

// Тесты могут пройти, но при определённой синхронизации
// потоков произойдет deadlock в production!

7. Внешние зависимости

public class PaymentGatewayClient {
    public boolean processPayment(String cardToken, BigDecimal amount) {
        // Вызов реального API платежной системы
        HttpResponse response = httpClient.post("https://payment-api.com/charge", 
            cardToken, amount);
        return response.isSuccess();
    }
}

@Test
public void testPayment() {
    PaymentGatewayClient client = new PaymentGatewayClient();
    assertTrue(client.processPayment("tok_valid", new BigDecimal("100")));
    // ✓ Пройдёт, если API доступна
    // ❌ Но что если API недоступна или вернула ошибку в production?
}

// Нужно мокировать внешние зависимости

Признаки того, что тесты неполные

public class QualityMetrics {
    // 1. Низкое покрытие кода (coverage < 80%)
    // 2. Нет тестов для исключений
    // 3. Нет интеграционных тестов
    // 4. Тесты очень быстрые (может быть скрывают реальные проблемы)
    // 5. Нет тестов для граничных случаев
    // 6. Нет тестов для многопоточности
    // 7. Нет performance тестов
    // 8. Нет security тестов
}

Правильный подход

public class ComprehensiveTestingStrategy {
    /*
    1. Unit тесты:
       - Все логические ветки
       - Граничные случаи
       - Исключения
    
    2. Интеграционные тесты:
       - Реальная БД (или тестовая)
       - Реальные API (или VCR/mock servers)
    
    3. E2E тесты:
       - Полный flow пользователя
    
    4.性能 тесты:
       - Load testing
       - Stress testing
    
    5. Security тесты:
       - SQL injection
       - XSS
       - CSRF
       - Authentication/Authorization
    */
}

Цитата из индустрии

"Тесты показывают отсутствие ошибок только при условиях, которые вы тестировали. Они не могут доказать полную корректность." — Dijkstra

Итог

Зелёные тесты — это необходимое условие, но не достаточное. Код может быть:

  • Функционально правильным в протестированных сценариях
  • Содержать баги в непокрытых случаях
  • Уязвим к security атакам
  • Падать под нагрузкой
  • Иметь race conditions

Корректность достигается через:

  1. Полное покрытие тестами (>90%)
  2. Code review от опытных разработчиков
  3. Статический анализ кода
  4. Интеграционное тестирование
  5. Production мониторинг
  6. Security аудит
  7. Load тестирование