Отладка. Step-by-step к эффективному выявлению ошибок

Каждый разработчик сталкивается с необходимостью исправления дефектов/багов/ошибок в разрабатываемых программах. И к сожалению, согласно результатам анализа, приведенным в книге Стива Макконнела «Совершенный код», статистика показывает, что мы затрачиваем на это довольно много своего рабочего времени. Даже стопроцентное покрытие тестами кода не дает гарантии отсутствия ошибок. Сократив время отладки, исправления и предупредив возникновение новых дефектов, мы сохраним время на то, что нам нравится больше всего, — на решение сложных задач. К тому же это существенно уменьшит затраты на разработку продукта.

Я много взаимодействую с разработчиками в команде: менторинг, консультирование, обучение, парное программирование, код-ревью. В процессе становится понятно: ошибки допускают все — от новичка, только ступившего на карьерную тропу, до матерого разраба с многолетним опытом.

И эти наблюдения привели меня к формированию чек-листа, который помогает нам в команде быстро найти проблему, а также предотвратить возникновение новых.
Цель этой статьи — описать системный, поэтапный процесс выявления и предотвращения ошибок.

Запасайтесь кислородом, нас ждет погружение в пучину багов.

Причины возникновения ошибок

Обычно возникновение ошибок связано с написанием или изменением кода. Стоит понимать, что 99% ошибок — это ответственность разработчика. А вот с причинами их возникновения будем разбираться дальше.

Позволю себе выделить такие категории причин (по приоритету сложности обнаружения):

  • опечатки и невнимательность;
  • непонимание логики участка кода;
  • незнание тонкостей языка разработки;
  • ложные тесты;
  • отсутствие обработки ошибок;
  • копирование чужих ошибок.

Рассмотрим каждую категорию на примерах. Язык программирования практически не имеет значения. Примеры будут на JavaScript, однако такой же принцип применим и к другим языкам.

Опечатки

Мозг работает быстрее, чем наши пальцы. Неудивительно, что допускаются автоматические ошибки в коде.

Кейс из жизни. Около часа не могли понять, почему падает тест, проверяющий в поле значение Сontact. При проверке кода теста у себя (с включенным spellchecker) была выявлена кириллическая С.


Анализаторы кода (Roslyn, ReSharper, SonarQube, ESLint и другие), code review и парное программирование помогут избавиться от этих ошибок.

Непонимание логики

Нередко мы сталкиваемся с кодом, написанным другим разработчиком либо же нами, но очень давно, и в таком случае необходимо понять работу существующей логики. Если же мы поняли ее неверно, вероятно, новый код будет с ошибкой. Вот способы, которые помогут разобраться в коде:

  1. Найдите автора кода. Разузнайте детали, задавайте много вопросов, чтобы убедиться, что поняли все верно. Если автор вы или его уже нет, идем дальше.
  2. Обратитесь к тестам. Хорошие тесты доступно описывают поведение кода.
  3. Расспросите QA, владельца продукта, аналитика, PM (или того, кто у вас занимает роль хранителя знаний о поведении этого продукта).

Незнание тонкостей языка разработки

Утечки памяти, неконтролируемая модификация данных, неверные расчеты — всех этих типов ошибок можно избежать, лучше изучив язык программирования, на котором вы разрабатываете.

Для входного параметра value = 10 условие тоже будет выполняться. Нужно добавить строгое сравнение ===.

Операция + для чисел выполняет сложение, а если один из параметров представлен строковым значением, то конкатенацию:

Свойство count было изменено по ссылке.
Кейс из жизни. При связывании контекста через bind аргументы в метод приходят в таком порядке: сначала параметры при связывании, потом параметры вызова. На это не обратили внимания при разработке, в итоге метод работал лишь по счастливой случайности при удачном порядке входных аргументов.
Валидаторы кода и статические анализаторы укажут на эти проблемы.

Невнимательность

К этой категории можно отнести следующие ошибки:

  • неучитывание граничного значения, к примеру < вместо <= (о граничных значениях и эквивалентных классах можно почитать здесь);
  • вызов метода с другой перегрузкой;
  • неверный порядок операций, например забыли () для выражения 1 + 2 * 3;
  • использование не того свойства/параметра.

Кейс из жизни. Был цикл динамического наполнения коллекции для combo box на 10 элементов. Позже первым элементом добавили статическое значение, не изменив цикл. В итоге последний элемент коллекции не отображался.

С такими ошибками хорошо справляется unit-тестирование.

Ложные тесты

Полагаю, это самый опасный вид «распространителя» ошибок. Когда мы уверены, что код покрыт «зеленым» тестом, мы вряд ли будем искать баг на этом участке.

К примеру, этот тест будет проходить даже в том случае, если у currentUser не будет заполнено свойство Id.

Писать более «правдивые» тесты поможет подход TDD. Рекомендую к прочтению книгу адепта TDD Roy Osherove.

Отсутствие обработки ошибок

В случае если мы вызываем методы, которые могут генерировать исключения, без какой-либо обработки, то это уже является ошибкой на нашей стороне. Нужно уметь обезопасить свой код от внешнего воздействия: проверять входные параметры (утверждения, граничные проверки, проверки типов), обрабатывать исключения, применять паттерн Null-object.

Проверка входных параметров защитит логику метода от использования с непредвиденными значениями параметров.

Обработка исключений необходима при вызове внешних модулей. Это обеспечивает гарантированное выполнение кода и реагирование на непрогнозируемые ситуации. При обработке исключений возможно журналирование, повторное выполнение, альтернативная ветка кода, проброс исключения дальше и т. д.

Паттерн Null-object сокращает количество проверок типа:

Он может быть реализован таким образом:

Копирование чужих ошибок

Копирование чужого кода может сократить время разработки в два раза и вчетверо увеличить время отладки. В интернете очень много готовых решений на шаблонные/простые задачи. И нередко возникает искушение «позаимствовать» этот код для своих нужд. Однако помните: в системе контроля версий будет ваше имя, а не имя автора первоначального кода.

Кейс из жизни. Скопированный код для работы с деревом почему-то не предусматривал сворачивания вложенных дочерних элементов. Ошибка проявилась, лишь когда появились данные для трехуровневой иерархии. Возможно, автор кода этого не учел, а может быть, ему и не нужно было предусматривать такой случай.

Предотвращение багов

Лучшая борьба с багами — это предотвращение их на этапе разработки. Для этого существует множество практик, методологий, инструментов. Парное программирование + TDD + анализаторы кода = стена, уверенно защищающая от «ходячих» багов. Вот некоторые из подходов:

Code review

Процесс ревью включает в себя привлечение коллег к чтению и анализу кода. Это помогает выявить машинальные, логические, структурные ошибки, опечатки. Важно, чтобы один из ревьюверов владел нужными знаниями и опытом. Эффективен практически для всех видов ошибок.

Парное программирование

Эта практика не только включает в себя постоянное code review в реальном времени, но и сокращает ошибки при разработке. Также может предупредить ошибки постановки, концептуальные ошибки архитектуры. Бонусом будет обмен опытом, который пригодится и при исправлении других ошибок.

Тестирование

Моментальная сигнализация о появлении бага — что может быть лучше? Именно эту возможность предоставляют нам тесты — быстрые, постоянные, эффективные. Тестирование как инструмент предотвратит ошибки вследствие несоблюдения логики, невнимательности, отсутствия учета тонкостей языка программирования. Стандартная пирамида тестирования включает в себя приемочные, функциональные, системные, интеграционные, unit-тесты. К отдельному виду можно отнести мутационные тесты .

Ниже приведены unit-тесты, которые покажут ошибки в малейшем изменении поведения.

TDD

Хорошие тесты — правдивые тесты. Чтобы доверять своим тестам, рекомендуем применять разработку через тестирование. С таким подходом ваши тесты будут проверять лишь то, что нужно, и будет повышаться уверенность в их качестве. К тому же сокращается количество ненужных строк кода. Подробнее можно прочесть тут.

Анализаторы кода

Множество орфографических, структурных ошибок, опечаток могут помешать правильной настройке IDE с анализаторами кода. Статические анализаторы по типу SonarQube указывают на проблемы и подробно описывают, как их устранить.

Реализация атомарными частями

Коммитить чаще, релизить чаще — это привычки, которые потенциально сократят ошибки. Частые и маленькие коммиты — быстрое прохождение pipeline-тестирования и ранний feedback по качеству коммита. Этот подход поможет сократить время влияния ошибок на продукт. Чем дольше ошибка в коде, тем дороже будет ее устранение.

Делайте перерывы

Возможно, не самый технический совет, однако один из действенных. Регулярные перерывы снижают усталость, помогают сфокусироваться, удерживают в тонусе. Это приводит к более вдумчивой разработке, тем самым сокращая вероятность ошибки. Можете воспользоваться одной из проверенных техник — Pomodoro.

Поиск и устранение

Ну что ж, о том, чем баги страшны и как они зарождаются, мы поговорили. Самое время найти и «пофиксить» их. В идеале у вас должен быть полный список перечисленных инструментов (а может, и больше). Если же отсутствуют тесты, нет журналирования или VCS, то самое время задуматься об их внедрении. Затраты на внедрение с лихвой окупятся в дальнейшей перспективе разработки.

Методы расположены в порядке повышения значимости для соотношения «время — профит». Таким образом, применив один подход и перейдя к следующему, мы повышаем вероятность нахождения ошибки. Конечно же, можно начать с любого этапа, все зависит от исходных данных.

Воспроизвести

Получив issue от QA, первым делом нужно убедиться: а есть ли проблема? Нужно воспроизвести ошибку и удостовериться в стабильности ее возникновения.

Проверить актуальность изменений

Бывает, забыл сохранить во вкладке какой-то конфиг и час думаешь, почему не работает как надо. Поэтому стоит удостовериться, что все исходные коды, конфиги сохранены и все скомпилировано.

Понять бизнес-логику

Нельзя исправить (корректно) баг без понимания, что это за функционал. Ошибка могла зародиться еще на этапе постановки задачи.

Свериться со списком частых ошибок

Полезная практика — вести учет возникших ошибок, будь то некий чек-лист или база знаний. Сгруппированный список поможет всего за несколько минут определить и решить ранее выявленную проблему.

Свериться с тестами

Если они, конечно же, есть :) Тесты — это лучшая документация вашего кода. По ним всегда должно быть понятно, как работает ваша программа. Правдивые тесты не только покажут наличие ошибки, но и подскажут способ ее устранения. Повторюсь, правдивые.

Проанализировать журнал

Надеюсь, что журналирование в вашей системе есть. В debug-режиме сохраняйте всю необходимую для анализа информацию. Для production логируйте последовательность действий и ошибки. Грамотно составленный журнал позволит выявить проблему без перечитывания сотни строк. Еще больше информации даст подход event sourcing.

Исследовать систему контроля версий

Нередко момент появления бага можно определить с точностью до часа. «Еще вчера работало» — одна из избитых фраз разработчика. И VCS будет лучшим хранителем истории изменений. Посмотрев в log репозитория (blame файла), можно с уверенностью определить роковой commit. В этом случае осмысленные комментарии в коммите и связь с ticket-системой значительно облегчат понимание намерений автора.

Использовать инструменты отладки

Самое время воспользоваться мощными инструментами для анализа и выявления ошибок.

Средства профилирования (dotTrace, ANTS, SQL Profiler) — для поиска плавающих багов, утечек памяти, влияния на систему.

Дебагеры (ChromeTools, OzCode) — для отслеживания выполнения программы строка за строкой. Хорошее владение этими инструментами значительно упрощает поиск и исправление ошибок.

Наша компания предоставляет клиентам платформу, на которой они разрабатывают и внедряют свои процессы. Нередко бывали случаи, когда эти решения были настолько масштабными, что создавали значительную нагрузку на систему. Выявление причин возникновения этой нагрузки требовало значительных затрат времени. Поэтому мы автоматизировали процесс их выявления. Объединение нескольких инструментов анализа, сбора статистики, журналирования в единый pipeline свело затраты из-за ручной работы к нулю.

Эта тема требует детального рассмотрения. Ее я попробую раскрыть в одной из следующих статей.

Сделать гипотезу

Когда анализ не дает результатов, пора включать режим разработчика. Сделайте предположение на основании своих знаний и опыта, где же кроется ошибка. Внесите изменения и получите быстрый результат. Продвижение должно быть систематичным и контролируемым (по одному изменению за раз), чтобы это не превратилось в хаотичное изменение кода. Можно воспользоваться такими подходами:

  • метод откатов — откатывать изменения из VCS до тех пор, пока ошибка не исчезнет, тем самым определив нужный коммит;
  • сравнение версий — сравнить поведение и результат с предыдущей рабочей версией.

Эффект утенка

Если вы дошли до этого пункта, значит, на поиск ушло несколько часов. Вы изрядно устали, потеряли концентрацию, перестали фокусироваться. Воспользуйтесь методом резинового утенка. Расскажите своему коллеге, что вы уже сделали для обнаружения ошибки. Если это и не осенит вас во время обсуждения, то хотя бы коллега поможет советом.

Заключение

Мы рассмотрели способы, которые помогут сократить время поиска. Системный и структурированный подход к поиску и фиксации ошибок значительно сокращает затраты времени на их исправление. Даже 10% выигранного времени каждого разработчика принесут хорошую выгоду. И вы сможете уделять больше времени созданию, а не исправлению. Формат чек-листа помогает ничего не забыть и наладить системный процесс выявления ошибок. Возможно, вы используете другие подходы, которые помогают вам. Буду рад обсудить их в комментариях.

И помните: качество — синоним успеха!

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному2
LinkedIn



15 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Самое сложное искать баги связанные с многопоточной обработкой данных, так как часто порядок получения промежуточного результата может влиять на результат конечный. Это конечно происходит в совсем критических случаях когда изображение сильно размыто, зашумлено или снято на 0.3 Мпикс камеру (качество очень похожее), но приходится бороться с таким. Однопоточка уже особо не тянет алгоритмы.

Спасибо за статью! Есть интересные моменты

Бо́льшая часть статьи напоминает телепередачи про садоводство отсюда: «Надеюсь, вы успешно применили разработанную вами машину времени и уже N лет работаете по нашему плану развития Идеального Продукта. И теперь вы можете воспользоваться этим, когда к вам таки заполз баг.»
Но к теме, объявленной в статье (что делать, когда таки есть баг и нужна отладка), это имеет отношение, близкое к нулю. А реплики типа такого:

Если вы дошли до этого пункта, значит, на поиск ушло несколько часов.

хоть и дословно верные — в данном контексте выглядят так же чужеродно, как тот резиновый утёнок в пчелином улье.
Или это:

Нужно воспроизвести ошибку и удостовериться в стабильности ее возникновения.

тупо неверно, если ошибка нестабильна, или она реально проявляется у пользователя в его специфической конфигурации, но не в той, которая у пользователя (ещё Спольски писал об этом, см. про ошибку на польской Windows). Туда же про двукратный дифирамб TDD. Все ваши примеры из веба, где подход «внутреннего ПО» (по той же статье Спольски) неприменим в принципе.

В результате получился самый настоящий clickbait: броский заголовок не соответствует содержанию, банальному по форме и сомнительному по сути.

Валентин, я поделился теми подходами, которые применяю и которые помогают мне в работе. И большинство из них применимы не только для веба. А какими методиками вы пользуетесь для поиска и устранения ошибок?

Дело не в том, что вы применяете эти подходы и они помогают. Да, помогают. Дело в том, что состав подходов на 90% не соответствует заголовку, если рядом с теми рецептами вы бы написали, что занимаетесь физкультурой и регулярно чистите зубы — результат был бы ровно таким же: не по сути. И зачем это надо было писать — совершенно непонятно.

Возможно, было бы лучше написать в заголовке статьи — «Путь к минимизации количества ошибок». Но это уже другая тема статьи.

Если же отбросить всё ненужное, и сосредоточиться только на той части, что представляет собой действия после нахождения ошибки — то тут уже появляется конкретика, которую можно свести к следующему:

1. Да, воспроизведение ошибки, стабильное или хотя бы вероятностно успешное (! — вы это пропускаете). «Происходит в 10% случаев» лучше, чем вообще без воспроизведения, хоть и хуже, чем в 100%. Отсюда можно брать параметры окружения для исследования. Если есть хотя бы подозрение на триггер-фактор — то тоже.

К этому же вопрос о корректности представления об окружении. Типовая админская ситуация «ой, забыл отредактировать один конфиг из 50 необходимых» — как раз отсюда. Вы это упомянули, но вскользь, хотя это в сложной среде — одно из самых важных.

2. Сбор сопутствующих данных — в основном это логи, если есть. Если нет — попробовать включать (если есть уже готовое; если нет — см. ниже). Если логи устраняют ошибку своим присутствием — самый неприятный вариант (гейзенбаг), но и его можно попробовать разобрать логически — хоть это и сложнее.

3. Теоретически локализовать возможную область ошибки.

4. Если возможно, найденную область укреплять в коде — больше проверок ошибок (включая параноидальные на вроде бы невозможное), больше логов. Сюда же — расширение состава тестов (больше предполагаемых типовых, больше маргинальных вариантов).

5. Если есть возможность (есть далеко не всегда), останов под отладчиком и исследование текущих состояний. Когда доступно — помогает хорошо. Но главное — не задолбаться многосоткратными повторениями: если прогоняешь один сценарий вручную под отладчиком более 2 раз — надо искать другой подход. Некоторые отладчики позволяют автоматизировать заметную часть подготовки до вхождения в проблемный период — этим надо пользоваться.

В последовательности выше возможны (и часто будут) откаты на предыдущие шаги.

После того, как баг локализован и опознан, надо его исправить — очевидно, но типовым вариантом является закрепить его фикс в виде теста. Как будет выглядеть этот тест — зависит от местности, общим рецептом тут является разве что «чем ниже — тем лучше»: юнит-тест одной функции лучше чем тест модуля, модуля — чем подсистемы, простой тест со входами-выходами лучше сценарного, и так далее.

Теперь — что у вас было в статье.

Опечатки, незнание логики, незнание тонкостей, невнимательность, ложные тесты, неигнорирование ошибок, парная разработка, ревью, отдых вовремя и ещё 100500 других: это всё не вопрос «выявления ошибок». Это вопрос качества процесса разработки кода. Связывать одно с другим — грубейшая диверсия против принципов.

TDD: диверсионная методика, которая некорректно компонует изначально верные принципы. По чистому TDD можно делать только однократную работу по уже известному до деталей плану, когда всё уже точно известно заранее и нет места даже для мельчайшей работы по исследованию реализации. Это противоречит, кстати, Agile (всем его формам), и любая реальная работа как-то его нарушает.

Null object: опять-таки вопрос стиля и архитектуры, причём очень частный приём — где-то он годится, но в очень многих случаях — нет. Выносить его в рекомендации — почему не вынесено 125+ других паттернов разработки? Давайте их всех перечислять.

Из полезного и одновременно соответствующего заголовку — остаётся:
1. Общий сценарий на ошибку (см. выше).
2. Сверка с тестами. Это отдельная обширная тема, но в целом сравнение с тестами требует уже пройдённой локализации ошибки — чтобы было, что сравнивать. И вот что делать дальше с этим — вопрос плавно переходит в качество ТЗ на компоненты, а может, и более глобального.
3. Event sourcing. Да, где он возможен — для воспроизведения полной обстановки перед ошибкой — это примерно равно скриптованию отладки. Увы, ой не всегда возможно.
4. Поиск коммита — источника ошибки (bisect и тому подобные). Очевидно для опытного разработчика, но не юниоров. И средство должно такое позволять (соболезную тем, кто завязан волей судьбы на старые тупые VCS).

Статью можно было бы доработать, разбив реально на 3-4:
1. По общему качеству кода через качество процесса разработки.
Сюда же относится вообще минимизация использования языков программирования там, где можно обойтись без них.
2. Средства и методики, которые заранее предусмотрены для облегчения диагностики обнаруженных багов.
3. Собственно локализация и опознание.
4. Действия после нахождения собственно исправления. Частично пересекается с пунктом 1, но существенно, что источники требований разные. Например, в тестах всех видов 1 даёт в основном «поведенческие» тесткейсы (согласно ТЗ), а 4 — маргинальные и регрессионные.
(Разумеется, можно постить, объединяя группы — но адекватно их назвав при этом.)

Валентин, спасибо за столь подробный комментарий! В статье я раскрываю причины возникновения, методики поиска и способы предотвращения ошибок. Разделение на отдельные статьи было бы неконсистентным. Я хотел охватить максимальную аудиторию наполняя контентом «от простого к сложному». Возможно название и не отражает всей сути, учту в дальнейших статьях.

Для того, щоб було менше багів, треба менше писати коду та більше конфігурувати або використовувати DSL (domain specific language).
З повагою, ваш Кеп.

Типы забыл.

У них же JS во всех примерах.

Это тот, который не нужен?

Спасибо за статью.

PS.

метод резинового утенка

 — боюсь окружающие не поймут и вызовут санитаров :)

Просто пообщайся с коллегой или опиши проблему в чате. Часто решение приходит уже в процессе описания проблемы.

Є люди витрачають свій вільний час на написання статей (які можна не читати якщо не цікаво)
А є ті, що витрачають його на useless коменти
Як на мене користі більше від перших.
#makeLoveNotWar ))

Спасибо, Виктор, что вам не безразлична эта тема. Просто в офисе закончился смузи, а настольный футбол был занят. Вот я и решил перед занятием по йоге написать пару строчек и картинку нарисовать :)

Підписатись на коментарі