Инженерные основы: отладка, тестирование, контроль версий, качество кода
Фундаментальные знания из предыдущих статей курса дают правильную модель мышления:
мы строим модель задачи и данных
выбираем абстракции и границы
оцениваем стоимость операций через Big O
помним, что реальная производительность упирается в память, кэш и I/OИнженерные практики делают эти идеи управляемыми в реальном проекте:
отладка помогает быстро находить, где модель расходится с реальностью
тестирование фиксирует инварианты и не даёт системе «тихо сломаться»
контроль версий делает изменения обратимыми и объяснимыми
качество кода снижает стоимость изменений и ошибокЭта статья про практические навыки, которые превращают «я написал код» в «я могу поддерживать систему годами».
Отладка как поиск расхождения модели и реальности
Отладка начинается не с инструмента, а с вопроса: какое утверждение в моей модели неверно? Например:
я думал, что список отсортирован, но он не отсортирован
я думал, что операция , но она запускает дорогой системный вызов
я думал, что два потока не трогают общий объект, но трогаютМинимальный цикл отладки
!Цикл отладки: как системно двигаться от симптома к исправлению
Практический цикл выглядит так:
Зафиксируйте симптом: что именно не так и где это видно.
Воспроизведите проблему: сделайте так, чтобы ошибка повторялась.
Сформулируйте гипотезу: что должно быть истинно, но не истинно.
Наблюдайте: соберите факты через логи, дебаггер, метрики.
Минимизируйте: найдите самый маленький вход или сценарий, который ломает систему.
Исправьте причину: меняйте не симптом, а источник расхождения.
Зафиксируйте знание тестом: добавьте регрессионный тест.Ключевая идея: если проблема не воспроизводится, вы не управляете процессом отладки.
Инструменты наблюдения
#### Логи
Логи полезны, когда:
баг проявляется только в окружении, куда нельзя подключить дебаггер
проблема связана с конкуренцией или редкими таймингами
нужно собрать контекст: входные параметры, идентификаторы, порядок событийПрактические правила:
логируйте границы (вход/выход) и инварианты (ожидаемые условия)
добавляйте корреляционный идентификатор запроса, чтобы склеивать события
избегайте логов «на каждый шаг» в горячих циклах, это может убить производительность#### Дебаггер
Дебаггер помогает «заморозить мир» и посмотреть состояние программы.
Что обычно делают:
ставят breakpoint (точку остановки)
смотрят стек вызовов, локальные переменные, значения выражений
выполняют код построчноПолезное понятие: стек вызовов показывает, какие функции привели к текущей точке. Это напрямую связано с тем, как память выделяется под вызовы функций (стек), о чём мы говорили в статье про устройство компьютера. См. Call stack.
#### Профилирование
Если «всё правильно, но медленно», нужен не дебаггер, а профилировщик.
Профилирование отвечает на вопросы:
где тратится CPU-время
где много аллокаций и давление на сборщик мусора
какие функции вызываются слишком частоЭто практическое продолжение тем Big O и структур данных: асимптотика говорит как растёт, профилирование показывает где именно больно сейчас.
#### Трассировка и метрики
Для распределённых систем важны:
метрики (время ответа, ошибки, очереди, использование CPU/RAM)
трассировка запросов между сервисамиОдна из причин «странной медленности» часто не в алгоритме, а в I/O и ожиданиях (сеть, диск, база, блокировки), что напрямую связано с темой ОС и процессов.
Частые классы ошибок и как их распознавать
| Симптом | Частая причина | Что проверить сначала |
| --- | --- | --- |
| «Иногда падает» | гонка потоков, неинициализированное состояние | общий доступ к данным, критические секции |
| «Иногда медленно» | GC-пауза, page fault, блокировки, rehash | хвосты latency, пики аллокаций, локи |
| «Работает локально, не работает в проде» | конфиг, окружение, данные, права | различия конфигов, версии, входные данные |
| «Долго грузит и много памяти» | слишком много объектов, плохая локальность | структура данных, аллокации, кеширование |
Тестирование как фиксация инвариантов
В первой статье курса мы говорили про инварианты: условия, которые должны быть истинны всегда в рамках модели данных. Тесты нужны, чтобы эти условия были проверяемы автоматически.
Тестирование отвечает на два вопроса:
что система делает правильно (корректность)
что не сломалось после изменений (регрессия)См. обзорно: Software testing.
Уровни тестов
!Пирамида тестов: баланс скорости, надёжности и покрытия
Определения:
Юнит-тест проверяет маленький кусок логики изолированно, обычно функцию или класс.
Интеграционный тест проверяет взаимодействие компонентов, например код + база данных.
Сквозной тест (E2E) проверяет сценарий целиком, как пользователь: UI или API + все зависимости.Практический принцип: чем выше уровень теста, тем он обычно медленнее и сложнее в сопровождении.
Что именно тестировать
Хорошие кандидаты для тестов:
чистые функции и бизнес-правила (валидаторы, расчёты, переходы состояний)
критичные инварианты (например, «баланс не уходит ниже лимита»)
ошибки на границах (пустые значения, большие размеры, неверный формат)Плохие кандидаты:
детали реализации, которые вы хотите свободно менять
поведение, зависящее от таймингов, если это можно переписать детерминированноМоки, стабы и тестовые двойники
Если тест зависит от внешнего мира (сеть, база, время), он становится нестабильным.
Чтобы изолировать логику, используют тестовые двойники:
стаб возвращает заранее заданный ответ
мок дополнительно проверяет, как именно его вызвалиВажно не превратить тесты в проверку «какие методы я вызвал», иначе вы фиксируете реализацию, а не поведение.
Флейки-тесты
Флейки-тест — тест, который иногда проходит, иногда падает при одинаковом коде.
Типовые причины:
гонки и параллельность
зависимость от времени и таймеров
общий изменяемый глобальный стейт между тестами
реальная сеть или нестабильные внешние сервисыФлейки-тесты опасны, потому что команда перестаёт доверять тестам, а значит теряет главное преимущество автоматической проверки.
Покрытие кода и его ограничения
Покрытие показывает, какие строки или ветки кода выполнялись тестами. Оно полезно как сигнал, но не как цель.
высокое покрытие не гарантирует, что проверены правильные свойства
низкое покрытие почти всегда означает риск, особенно в критичной логикеСм. Code coverage.
Контроль версий как «память проекта»
Контроль версий отвечает на вопросы:
что изменилось и почему
кто и когда это сделал
как вернуться к рабочему состояниюСтандарт де-факто в индустрии — Git. См. Git и Pro Git.
Базовые понятия Git
репозиторий хранит историю изменений
коммит фиксирует снимок состояния проекта и описание изменения
ветка указывает на последовательность коммитов
слияние (merge) объединяет изменения двух ветокПрактическая привычка: делайте коммиты маленькими и смысловыми, чтобы историю можно было читать как объяснение проекта.
Сообщения коммитов
Хорошее сообщение коммита отвечает на вопрос что и зачем, а не как.
Пример:
плохо: fix
хорошо: Fix validation: reject empty email to keep uniqueness invariantЭто связывает контроль версий с темой инвариантов из первой статьи.
Код-ревью
Код-ревью — это не «проверка новичка», а механизм качества.
Что обычно ловят на ревью:
нарушения инвариантов и неочевидные граничные случаи
ухудшение асимптотики или лишние аллокации
плохие границы модулей и смешение уровней абстракции
небезопасные изменения в конкурентном кодеПоиск коммита, который сломал систему
Если «вчера работало, сегодня нет», полезно найти точку поломки.
Git даёт инструмент git bisect, который делает бинарный поиск по истории коммитов: вы помечаете известный хороший коммит и известный плохой, а Git помогает быстро найти первый плохой.
Это прямое применение идеи деления пополам из алгоритмов и мышления, но к истории проекта.
Качество кода как снижение стоимости изменений
Качество кода — это не про «красоту», а про способность безопасно менять систему.
Определение, удобное для практики:
качественный код легко читать
в нём ясно, где границы и ответственность
он защищён тестами на уровне инвариантов
его производительность предсказуемаЧитаемость и когнитивная нагрузка
Код читают чаще, чем пишут.
Практические правила:
имена должны отражать модель предметной области
функции должны делать одну понятную вещь
сложные участки должны быть оформлены через понятные абстракции, а не через комментарии к хаосуСвязь с темой абстракций: хороший интерфейс скрывает детали и делает невозможные состояния труднодостижимыми.
Границы модулей и зависимости
Ключевой вопрос качества: что от чего зависит.
Плохой признак:
доменная логика зависит от деталей SQL, HTTP, UIХороший признак:
доменная логика выражена отдельно и тестируется быстроЭто продолжает идею «не смешивать уровни абстракции» из первой статьи.
Линтеры, форматтеры и статический анализ
Инструменты качества помогают автоматизировать рутину:
форматтер делает стиль единообразным
линтер ловит подозрительные места
статический анализ может найти ошибки типов, неинициализированные значения и утечкиСмысл не в том, чтобы «угодить инструменту», а в том, чтобы снять с человека механическую проверку.
Рефакторинг и технический долг
Рефакторинг — изменение внутренней структуры кода без изменения внешнего поведения.
Почему он нужен:
требования меняются, и код должен оставаться гибким
накопление временных решений увеличивает риск ошибок и стоимость измененийТехнический долг — метафора того, что быстрые решения сегодня могут стоить дороже завтра. См. Technical debt.
Хорошая практика: рефакторинг проще и безопаснее делать маленькими шагами, когда есть тесты, которые фиксируют поведение.
Качество и производительность
Производительность часто «вшита» в качество дизайна:
неудачная структура данных даёт лишние в горячем месте
лишние системные вызовы превращают быстрый код в медленный из-за I/O
плохая локальность памяти может сделать «нормальный алгоритм» в разы медленнееПоэтому качество кода включает и дисциплину измерений: профилировать, а не гадать.
Практический рабочий процесс
Ниже — типичный поток изменений, который связывает всё вместе.
Опишите изменение через модель и инварианты.
Сделайте маленькую ветку.
Добавьте или обновите тесты на нужном уровне.
Реализуйте изменение.
Запустите линтеры, тесты и базовые проверки.
Откройте pull request и пройдите код-ревью.
Слейте в основную ветку.
Если что-то пошло не так, используйте историю коммитов и наблюдаемость для быстрой диагностики.Итоги
Отладка — это системный поиск расхождения между моделью и реальностью, основанный на воспроизведении и наблюдении.
Тесты фиксируют инварианты и защищают от регрессий; уровень теста определяет его скорость и стоимость сопровождения.
Git делает изменения объяснимыми и обратимыми, а git bisect превращает поиск поломки в управляемый процесс.
Качество кода — это читаемость, границы, предсказуемость и способность меняться без страха.Следующий шаг развития этих тем обычно идёт в сторону конкурентности, сетевого взаимодействия и системного дизайна: там без наблюдаемости, тестов и дисциплины изменений большие системы становятся неуправляемыми.