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

Как определяешь где нужно пофиксить баг

1.3 Junior🔥 181 комментариев
#Soft Skills и карьера#Тестирование

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

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

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

Как определяешь где нужно пофиксить баг

Отладка — это системный процесс, а не случайный поиск. Профессионально подходить к этому помогает метод научного тестирования (Scientific Method), известный как Divide-and-Conquer. Рассмотрю полный процесс.

Фаза 1: Воспроизведение проблемы

Шаг 1: Получи полную информацию о баге

// Вопросы, которые нужно задать:
/*
1. ЧТО именно произошло?
   - Какое сообщение об ошибке?
   - Какой результат получился?
   - Какой результат ожидался?

2. КОГДА это произошло?
   - При каких конкретных данных?
   - В какой версии кода?
   - Повторяется ли это всегда или случайно?

3. ГДЕ это произошло?
   - На каком шаге процесса?
   - В каком браузере/среде?
   - В каких условиях (нагрузка, память, сеть)?

4. КТО это нашел?
   - Какова вероятность воспроизведения?
   - Есть ли пошаговые инструкции воспроизведения?
*/

Шаг 2: Создай минимальный тестовый случай (Minimal Reproducible Example)

@Test
public void reproduceNullPointerBug() {
    // Минимальный код для воспроизведения
    User user = new User(null, "john@example.com");
    String result = user.getFullName();  // NPE здесь!
    
    assertNotNull(result);
}

// Хорошие MRE:
// - Только необходимый код
// - Понятное ожидание vs. реальность
// - Легко запустить
// - Документированы входные данные

Фаза 2: Генерирование гипотез

Используй метод исключения (Process of Elimination)

public class BugHypotheses {
    /*
    Баг: OrderService.calculateTotal() возвращает неправильную сумму
    
    Гипотеза 1: Проблема в методе calculateTotal() ✓ (начни отсюда)
    Гипотеза 2: Проблема в getOrderItems()
    Гипотеза 3: Проблема в getPrice() для товаров
    Гипотеза 4: Проблема в базе данных (неправильные данные)
    Гипотеза 5: Проблема в округлении (double precision)
    Гипотеза 6: Проблема в валюте/конвертации
    
    Приоритет: Начни с кода, работающего с самыми свежими данными
    */
}

Фаза 3: Логирование и отладка

Стратегия 1: Binary Search (Бинарный поиск)

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        // Добавь логирование на каждом шаге
        List<OrderItem> items = order.getItems();
        logger.debug("Items count: {}", items.size());  // Step 1
        
        BigDecimal total = BigDecimal.ZERO;
        
        for (OrderItem item : items) {
            BigDecimal itemPrice = item.getPrice();
            logger.debug("Item price: {}", itemPrice);  // Step 2
            
            int quantity = item.getQuantity();
            logger.debug("Item quantity: {}", quantity);  // Step 3
            
            BigDecimal subtotal = itemPrice.multiply(
                new BigDecimal(quantity)
            );
            logger.debug("Item subtotal: {}", subtotal);  // Step 4
            
            total = total.add(subtotal);
            logger.debug("Running total: {}", total);  // Step 5
        }
        
        // Применяем скидку
        BigDecimal discount = order.getDiscount();
        logger.debug("Discount: {}", discount);  // Step 6
        
        total = total.subtract(discount);
        logger.debug("Final total: {}", total);  // Step 7
        
        return total;
    }
}

Стратегия 2: Использование отладчика (IDE Debugger)

// В IntelliJ IDEA:
// 1. Установи Breakpoint на подозрительной строке
// 2. Запусти код в Debug режиме (Shift+F9)
// 3. Используй Step Over (F10) для пошагового выполнения
// 4. Используй Evaluate Expression (Alt+F9) для проверки значений
// 5. Смотри Variables panel для изменения переменных

public BigDecimal calculateDiscount(BigDecimal total) {
    // Breakpoint здесь (Ctrl+Shift+B)
    if (total.compareTo(BigDecimal.valueOf(100)) > 0) {
        return total.multiply(BigDecimal.valueOf(0.1));
    }
    return BigDecimal.ZERO;
}

Стратегия 3: Conditional Breakpoints

// Breakpoint срабатывает только при условии
for (OrderItem item : items) {
    // Правый клик на Breakpoint -> Filter:
    // item.getPrice().compareTo(BigDecimal.ZERO) <= 0
    // Останавливается только на нулевых/отрицательных ценах
    total = total.add(item.getPrice());
}

Фаза 4: Анализ и локализация

Техника Stack Trace анализа

Exception in thread "main" java.lang.NullPointerException
    at com.example.OrderService.calculateTotal(OrderService.java:45)  ← Здесь!
    at com.example.OrderController.getOrder(OrderController.java:12)
    at java.base/java.lang.Thread.run(Thread.java:829)

Почини в обратном порядке стека:
1. OrderService.java:45 — основная ошибка
2. OrderController.java:12 — что передалось туда
3. Thread — где вызвалось

Анализ логов

# Посмотри логи приложения
grep "ERROR" app.log | head -20

# Найди тренд ошибок
grep "NullPointerException" app.log | wc -l

# Контекст ошибки
grep -B 5 -A 5 "NullPointerException" app.log

Фаза 5: Изоляция проблемы

Техника Rubber Duck Debugging

public class RubberDuckDebugging {
    /*
    Объясни код вслух (или напиши) линия за линией:
    
    1. User user = getUserFromDatabase(id);
       "Получаю пользователя из базы"
       
    2. String email = user.getEmail();
       "Получаю email из пользователя"
       
       СТОП! Что если user == null?
       А что если getEmail() возвращает null?
    */
}

Техника Bisecting (Git Bisect)

# Если баг появился недавно, используй бинарный поиск по коммитам
git bisect start
git bisect bad HEAD      # Текущая версия — плохая
git bisect good v1.0    # Версия v1.0 — хорошая

# Git автоматически переключится на коммит в середине
# Запусти тест, скажи git bisect bad или git bisect good
# Повторяй, пока не найдёшь точный коммит с ошибкой

Фаза 6: Проверка гипотезы

Создай тест, который падает с текущей ошибкой

@Test
public void testCalculateTotalWithNullDiscount() {
    Order order = new Order();
    order.addItem(new OrderItem("Product", BigDecimal.TEN, 2));
    order.setDiscount(null);  // Баг здесь!
    
    // Этот тест падает с NPE
    BigDecimal total = orderService.calculateTotal(order);
    assertEquals(new BigDecimal(20), total);
}

Запусти тест и подтверди, что он падает

$ mvn test -Dtest=OrderServiceTest#testCalculateTotalWithNullDiscount

FAILURE: testCalculateTotalWithNullDiscount
Expected: 20
Actual: NullPointerException ← Баг подтвержден!

Фаза 7: Исправление и валидация

Исправь минимально

// ❌ Не делай полный рефакторинг вместе с фиксом
public BigDecimal calculateTotal(Order order) {
    BigDecimal total = BigDecimal.ZERO;
    for (OrderItem item : order.getItems()) {
        total = total.add(item.getPrice());
    }
    
    // FIX: Проверка null
    if (order.getDiscount() != null) {
        total = total.subtract(order.getDiscount());
    }
    
    return total;
}

Запусти тест снова — теперь он должен пройти

$ mvn test -Dtest=OrderServiceTest#testCalculateTotalWithNullDiscount
SUCCESS: testCalculateTotalWithNullDiscount ✓

Запусти все тесты — убедись, что ничего не сломал

$ mvn test
...
[INFO] Tests run: 156, Failures: 0, Errors: 0 ✓

Практический пример: Отладка медленного запроса

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public List<User> searchUsers(String pattern) {
        long start = System.currentTimeMillis();  // Шаг 1: Замер
        
        List<User> users = userRepository.findByNameContaining(pattern);
        long dbTime = System.currentTimeMillis() - start;
        logger.info("Database query took: {} ms", dbTime);  // Где время теряется?
        
        // Шаг 2: Проверим данные
        logger.info("Found {} users", users.size());
        
        // Шаг 3: Проверим обработку
        start = System.currentTimeMillis();
        List<User> processed = users.stream()
            .filter(u -> u.isActive())
            .map(this::enrichUserData)  // Подозрительно!
            .collect(Collectors.toList());
        long processingTime = System.currentTimeMillis() - start;
        logger.info("Processing took: {} ms", processingTime);
        
        return processed;
    }
    
    private User enrichUserData(User user) {
        // Шаг 4: Проверим эту функцию
        // Может быть, здесь N+1 query?
        user.setOrders(orderService.getUserOrders(user.getId()));
        user.setNotifications(notificationService.getUnread(user.getId()));
        return user;
    }
}

// Вывод логов:
// Database query took: 50 ms
// Found 100 users
// Processing took: 5000 ms  ← БАГО! 5 секунд на 100 пользователей = 50ms/пользователь
// Вероятная причина: N+1 query при enrichUserData

Checklist отладки

[ ] Воспроизведу проблему с минимальным кодом
[ ] Написал тест, который падает
[ ] Логирую каждый шаг выполнения
[ ] Используешь отладчик для пошагового выполнения
[ ] Проверяю граничные случаи (null, empty, negative)
[ ] Смотрю Stack Trace и логи с полным контекстом
[ ] Проверяю версии зависимостей и JVM
[ ] Изолировал проблему до одной функции/класса
[ ] Исправляю минимально (одна проблема = один фикс)
[ ] Все тесты проходят (включая новый тест для баги)
[ ] Проверяю граничные случаи исправления
[ ] Документирую причину баги в комментарии/коммите

Best Practices

  1. Логирование информативное, не шумное:
// ❌ Плохо
logger.info("Started");
logger.info("Processing");
logger.info("Done");

// ✅ Хорошо
logger.info("Fetching user {} from database", userId);
logger.debug("User email: {}", user.getEmail());
logger.error("Failed to update user {}: {}", userId, exception.getMessage());
  1. Используй разные уровни логирования:

    • ERROR — исключения и критические ошибки
    • WARN — что-то неожиданное, но не критичное
    • INFO — важные события (запуск, завершение)
    • DEBUG — детали для отладки
  2. Версионируй по зависимостям:

mvn dependency:tree | grep problematic-library
  1. Используй профилирование:
@Profile("debug")
@Component
public class PerformanceMonitor {
    @Around("execution(* com.example..*(..))")  // AOP
    public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        Object result = pjp.proceed();
        long duration = System.nanoTime() - start;
        logger.debug("{} took {} ns", pjp.getSignature(), duration);
        return result;
    }
}

Итоги

  • Воспроизводимость — первый шаг (MRE)
  • Логирование — твой лучший друг
  • Отладчик — мощнее, чем логирование
  • Бинарный поиск — как в коде, так и в коммитах
  • Тесты — валидация исправления
  • Минимальный фикс — не переделывай код полностью
  • Документируй — почему был баг и как ты его нашел
Как определяешь где нужно пофиксить баг | PrepBro