Когда я работал в Яндексе, мы ежедневно сталкивались с багами, которые проскакивали мимо всех проверок. Помню один случай — в продакшене обвалилась часть сервиса только потому, что кто-то забыл обработать исключение при пустом ответе от внешнего API. Ловили это три часа. Потом я понял: поиск бага — это не просто тестирование. Это систематический процесс, который нужно организовать правильно.
Большинство разработчиков думают, что баги найдут сами, или полагаются на QA. Но если честно, самые хитрые ошибки видны только тому, кто написал код. И видны они не с первого раза.
Давайте разберёмся, как искать баги эффективно, какие инструменты использовать, и почему автоматизация здесь чуть ли не важнее, чем ручное тестирование.
Что такое баг и почему его сложно найти
Баг — это не просто ошибка в коде. Это поведение программы, которое отличается от ожиданий пользователя или спецификации. Может быть логическая ошибка, утечка памяти, состояние гонки, баг безопасности — много вариантов.
Сложность в том, что баг часто проявляется только при определённых условиях. Например:
- Когда сервер перегружен
- Когда интернет медленный
- Когда пользователь быстро кликает кнопку несколько раз подряд
- Когда данные в БД в определённом состоянии
- Когда используется конкретный браузер или версия ОС
Вот почему ручное тестирование всех сценариев невозможно. Даже если у тебя есть 100 тестировщиков, они не переберут все комбинации входных данных и состояний системы.
На одном проекте я видел, как баг воспроизводился только если отправить запрос ровно в момент, когда в базе запускается миграция. Вероятность — примерно 1 на 10 000. Нашли его случайно, когда смотрели логи в продакшене.
Ручное тестирование vs автоматизация
Честно? Нужны оба подхода. Но в разных пропорциях, чем думают большинство.
Ручное тестирование хорошо для:
- Проверки юзабилити и интерфейса
- Исследовательского тестирования (когда ты просто экспериментируешь)
- Поиска непредвиденных сценариев
- Первичной проверки новой фичи
Автоматизация находит то, что ручное тестирование пропустит:
- Граничные случаи (boundary conditions)
- Состояния гонки (race conditions)
- Утечки памяти и производительность
- Регрессии в коде
- SQL-инъекции и XSS
По статистике, автоматизированные тесты ловят примерно 70-80% багов. Ручное тестирование добавляет ещё 15-20%. Остальное — это мониторинг в продакшене и feedback от пользователей.
Вот почему у нас в Distiq — автоматический код-ревью анализирует каждый MR перед тем, как разработчик его вообще мержит. Это ловит много проблем на самом раннем этапе.
Как искать баги: практические методы
1. Граничные случаи (Boundary Value Testing)
Это первое, что нужно проверить. Если функция работает с числами, проверь:
- Минимальное значение
- Максимальное значение
- Значение 0
- Отрицательные числа
- Значения "на границе" (например, если максимум 255, проверь 254, 255, 256)
Классический пример — баг с переполнением. Вот код:
def calculate_discount(quantity):
"""Скидка: 10% за каждые 10 единиц"""
discount = (quantity // 10) * 10
return discount
# Это работает правильно
print(calculate_discount(50)) # 50%
# А это — нет
print(calculate_discount(100)) # Ожидаем 100%, получаем 100%
# Но что если скидка не может быть больше 50%?
Видишь проблему? Функция не ограничивает максимальную скидку. При количестве 1000 она вернёт 1000%, что глупо.
Правильный код:
def calculate_discount(quantity):
discount = min((quantity // 10) * 10, 50) # Максимум 50%
return discount
2. Состояния и переходы между ними
Каждый сложный объект в программе имеет состояния. Баги часто скрываются в переходах между ними.
Например, заказ может быть в состояниях: "создан", "оплачен", "отправлен", "доставлен", "отменён".
Вопросы для поиска багов:
- Может ли заказ перейти из "доставлен" в "оплачен"? (Не должно быть)
- Что происходит, если отменить заказ, когда он уже отправлен?
- Если платёж не прошёл, откатывается ли статус заказа?
Это не находится автоматически. Нужно думать.
3. Обработка ошибок и исключения
Большинство багов — это необработанные исключения или неправильная обработка ошибок.
def fetch_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json() # Что если сервер вернул 500?
Здесь целых три проблемы:
- Если сервер недоступен, будет исключение
- Если ответ некорректный JSON, будет исключение
- Если user_id = -1 или очень большой, API может вернуть 404, и мы попытаемся спарсить ошибку как JSON
Нормально это выглядит так:
def fetch_user_data(user_id):
try:
response = requests.get(
f"https://api.example.com/users/{user_id}",
timeout=5
)
response.raise_for_status() # Выбросит исключение на 4xx и 5xx
return response.json()
except requests.exceptions.Timeout:
logger.error(f"API timeout for user {user_id}")
return None
except requests.exceptions.RequestException as e:
logger.error(f"API error for user {user_id}: {e}")
return None
except ValueError as e:
logger.error(f"Invalid JSON response for user {user_id}: {e}")
return None
4. Конкурентность и race conditions
Если код работает с многопоточностью, асинхронностью или распределёнными системами, ищи race conditions.
# Опасный код
counter = 0
def increment():
global counter
counter += 1 # Это не атомарная операция!
# Если два потока вызовут increment() одновременно,
# counter может увеличиться только на 1 вместо 2
Правильно:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
with lock:
counter += 1
Типичные баги по языкам и фреймворкам
Python
1. Mutable default arguments
def add_item(item, list=[]): # БАГИ!
list.append(item)
return list
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — ОЖИДАЛИ [2]!
Список переиспользуется между вызовами. Правильно — list=None и создавать новый внутри.
2. Забывчивое закрытие ресурсов
# Плохо
f = open('file.txt')
data = f.read()
# Если здесь исключение, файл не закроется
# Хорошо
with open('file.txt') as f:
data = f.read() # Файл гарантированно закроется
JavaScript/TypeScript
1. Асинхронность и forgotten promises
// Баги!
function fetchData() {
fetch('/api/data')
.then(r => r.json())
.then(data => console.log(data))
// Ошибки не обработаны!
}
// Правильно
async function fetchData() {
try {
const r = await fetch('/api/data');
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
return data;
} catch (e) {
console.error('Failed to fetch:', e);
}
}
2. Null/undefined checks
// Может упасть
const user = getUser();
const name = user.profile.name; // Что если user или profile null?
// Правильно
const name = user?.profile?.name ?? 'Unknown';
SQL и базы данных
1. SQL-инъекции
# КРИТИЧЕСКИ ОПАСНО!
query = f"SELECT * FROM users WHERE email = '{email}'"
db.execute(query)
# Если email = "'; DROP TABLE users; --"
# Выполнится запрос: SELECT * FROM users WHERE email = ''; DROP TABLE users; --'
Правильно — всегда используй параметризованные запросы:
query = "SELECT * FROM users WHERE email = ?"
db.execute(query, (email,))
2. N+1 проблема
# Плохо — запросит БД 1000 раз
users = User.all()
for user in users:
print(user.profile.name) # Каждый .profile — отдельный запрос!
# Правильно — один запрос с JOIN
users = User.all().include('profile')
for user in users:
print(user.profile.name)
Инструменты для поиска багов
Статический анализ кода:
- pylint, mypy (Python)
- ESLint, TypeScript compiler (JavaScript)
- SonarQube (универсальный)
Динамический анализ (во время выполнения):
- Debugger (встроенный в IDE)
- Profiler (для performance bagов)
- Memory profiler (утечки памяти)
Тестирование:
- Unit-тесты (pytest, jest, unittest)
- Integration-тесты
- E2E-тесты (Selenium, Cypress, Playwright)
Property-based testing:
Вместо того чтобы писать конкретные тест-кейсы, описываешь свойства, которые должны быть верны для любых входных данных.
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(lst):
result = sorted(lst)
assert all(result[i] <= result[i+1] for i in range(len(result)-1))
Hypothesis автоматически генерирует тысячи тест-кейсов и ищет edge cases, которые ломают твой код.
Code review и AI-анализ:
Честно, после того как я начал использовать автоматический код-ревью, количество багов в production упало примерно на 40%. Инструмент смотрит на каждый MR и находит то, что QA и разработчик пропустили — необработанные исключения, неправильные проверки, небезопасные паттерны.
Где искать баги в production
Когда баг уже в боевой системе, мониторинг становится твоим лучшим другом.
Логи:
- Ищи stack traces и повторяющиеся ошибки
- Смотри корреляцию: когда начались ошибки? Что изменилось?
Метрики:
- Скачок ошибок
- Рост времени отклика
- Утечка памяти
User feedback:
- Жалобы в support
- Баги в issue tracker
Инструменты: Sentry, DataDog, Prometheus, ELK Stack.
Процесс поиска бага: пошагово
- Воспроизведи баг. Без воспроизведения ты не решишь его. Запиши шаги.
- Собери контекст. Версия кода, окружение, логи, метрики в момент ошибки.
- Выдвини гипотезы. Что может быть причиной?
- Проверь граничные случаи. Может ли баг быть связан с особыми значениями входных данных?
- Используй debugger. Запусти код пошагово и смотри переменные.
- Напиши тест. Тест, который воспроизводит баг. Потом исправляешь код так, чтобы тест прошёл.
- Проверь регрессии. Твой fix не сломал ничего ещё?
Культура поиска багов в команде
Тут важно не превращать поиск багов в охоту на ведьм. Если разработчик боится, что его код раскритикуют, он будет скрывать проблемы. Нужна культура, где:
- Баги видят как часть процесса, а не как личный провал
- Code review — это помощь, а не судилище
- Автоматизация берёт на себя рутину (поиск очевидных проблем)
- Люди фокусируются на логике и архитектуре
Если ты хочешь автоматизировать часть этого процесса, попробуй Distiq. Это AI-бот для code review, который анализирует каждый MR и находит баги, уязвимости и проблемы со стилем автоматически. Работает с GitHub, GitLab и GitVerse, интегрируется за две мин
