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

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

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

  1. Порядок событий. Когда происходит обработка события, полезно спрашивать себя: Могут ли события прийти не в том порядке? Что произойдет, если мы не получим событие? Что будет, если это сообщение о событии прийдет дважды? Даже если в нормальном состоянии системы, такое не должно происходить, ошибки в других частях системы могут вызывать такие эффекты.
  2. Слишком рано. Такие ошибки - это особый вид ошибок "Порядок событий". Например, если сообщение с сигналом о начале выполнения прийдет слишком рано (до конфигурирования и самого выполнения), то может наблюдаться очень странное поведение. Другой пример: соединение помечается оборванным, ещё до того, как оно перешло в статус ожидание(когда мы отлаживаем ошибки с соединениями, мы предполагаем, что упасть может соединение, которое находится в статусе ожидания). Это ошибочное предположение, не учитывает того, что события могут быть получены слишком рано.
  3. Молчаливые ошибки. Самые сложные ошибки, которые я отлаживал, появлялись в коде, скрытно: что-то в коде падало, но продолжало работать вместо того, чтобы выкинуть исключение. Например, системный вызов (bind) возвращал код ошибки, который не обрабатывался. Другой пример: парсер который возвращал пустой ответ вместо исключения, когда не мог обработать элемент. При этом вызов продолжается, но состояние системы уже было некорректно, что делало отладку значительно сложнее. Лучше решение - возвращать ошибку как можно раньше, после того как она произошла.
  4. If. Условия в коде с несколькими утверждениями if (a or b) особенно в виде if (x) else if (y) создало много проблем мне. Даже если сами условия концептуально просты, они могут создать проблемы, когда появляется зависимость от порядка проверки условия. Сейчас я стараюсь писать код так, чтобы избегать сложных условий сравнения.
  5. Else. Несколько ошибок были связаны с тем, что не было правильно определено, что должно происходить, если условие не выполняется. Практически в каждом if должна быть часть else. Если вы инициализируете переменную в одной части условия, то должны сделать это и в другой. Так же нужно поступать при инициализации флагов. Достаточно легко, добавить инициализацию флага только в условие, но забыть добавить в другое условие для сброс флага.
  6. Изменение предположений. Многие ошибки сложно предотвратить в первую очередь потому, что они появились из-за изменения предположений. Например, вначале работы системы предполагалось что один клиент мог делать только одну покупку в день. Большое количество кода было написано с учетом данного предположения. Но затем было принято решение изменить структуру программы для того чтобы позволить множественные покупки для клиентов. Когда такое происходит, достаточно сложно переписать все места, в которых использовалось старое предположение. Hайти все вхождения старого кода - это очень легко, но разобраться и найти все места, которые зависят от старого кода и его преположений значительно сложнее. Например, у нас в коде может быть запрос на получение списка всех покупок за день. При этом неявно в коде будет предполагаться, что количество покупок не больше, чем количество покупателей. У меня нет какой-то стратегии, как решать такие проблемы, так что я буду рад услышать ваши предложения.
  7. Логирование.Правильное понимаение того, что программа делает является очень критичным, особенно если программа имеет очень запутанную логику. Добавьте в программу достаточное количество (но не более) логирования, чтобы понимать что программа делает на самом деле. Когда все работает хорошо - это не на что не влият, но когда происходит проблемма, вы будете рады, что добавили логирование.

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

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

  1. Ноль и null. Проверьте, что вы всё тестировали и с нулем и с null (конечно, если это применимо к вам). Например для строки это означает, что нужно проверить и строку нулевой длинны и null-строку.
  2. Добавление и уменьшение. Часто новая фича добавляет новый в формат, например новый формат ввода телефона. Проверить работу с новым форматом - это естественно, однако я заметил, что часто при этом забывают проверить работу старого формата.
  3. Обработка ошибок. Тестировать код, который отвечает за обработку ошибок, достаточно сложно. Лучшим решением является создание автоматического теста, который будет вызывать нужную ошибку, однако это не всегда возможно. Для того, чтобы обойти этот момент, есть один фокус, который я иногда использую: изменить код для того чтобы вызывался обработчик ошибок. Самый простой способ - это изменить if, например if error_count > 0 на if error_count == 0.
  4. Случайный ввод. Один из способов найти ошибки - это ввод сулчайных значений. Например, ASN.1 декодируя H.323 работает с бинарными данными. Отправляя случайные последовательности бит, мы смогли найти несколько ошибок. Другой пример, это скрипт звонков, где продолжительность разговора, задержка перед ответом и паузы в разговорах были случайны. Этот скрипт помог найти несколько ошибок, в частности те, в которых события зависели друг от друга.
  5. Проверка, что что-то никогда не случится. Обычно тестирование заключается в проверке, что происходит в таком-то случае, но достаточно просто реализовать другую модель и проверять: что какое-то событие никогда не происходит.
  6. Собственные инструменты. Обычно я делаю какие-то собственные инструмены, для упрощения тестирования. Например, когда я работал на SIP протоколом для VOIP, я написал скрипт, который мог повторить запрос с точно такими же заголовками. Другой пример, это консольные скрипты для вызова API. Преимуществом написания собственных скриптов, является, то что вы получаете то, что было нужно именно вам.

Отладка

  1. Обсуждение. Техника отладки, которая больше всего помогла мне в нахождении ошибок: обсуждение с коллегами. Обычно достаточно объяснить коллеге в чем заключается проблема, чтобы понять как ее можно решить. Более того, даже если они не знакомы с сутью вопроса, они все равно могут подать хорошую идею. Обсуждение с коллегами наиболее эффективный способ решения сложных ошибок.
  2. Уделить повышенное внимание. Обычно если отладка занимает продолжительное время, то причина этого в неверных предположениях. Например, предлоложение, что проблема происходит в этом методе, хотя на самом деле, метод уже получает неверные данные. Или например, происходит исключение, которое не было предусмотрено. Для того чтобы это решить, нужно уточнять детали, вместо того чтобы строить предположения. Проще увидить, то что ты хочешь вместо того, что есть на самом деле.
  3. Последние изменения. Когда что-то работало, а потом перестало обычно это связано с последними изменениями. В отдельных случаях, последние изменения только показали большую проблему, которая была до этого скрыта. Возможность перейти на старую версию и проверить рабооспособность, помогает точно определить момент, когда проявляется проблема.
  4. Вера пользователю. Иногда получая от пользователя отчет об ошибки мы инстинктивно реагируем:"Это невозможно! Скорее всего он что-то сделал не так." Но со временем я научился не реагировать так, потому что чем чаще я проверял, тем чаще случаось то, что было описано в сообщении об ошибки. Существует большое количество нетипичных конфигураций и нетипичных действий, в которых предположения о том, как что работает оказывается неверным.
  5. Тестирование исправлений. Когда делается исправление оно должно быть протестировано. Вначале запускается код без исправления, и проверяется повторяемость ошибки. Потом запускается исправленный код и проверяется, что исправление решило проблему.

Другие наблюдения

Зв последние 13 лет, которые я записываю ошибки многое изменилось. Я работал над встроенными системама, над телекоммуникационными системами и над веб-проектами. Я работал с C++, Ruby, Java и Python. Другие ошибки, такие как краевые условия и ошиюки цикла, я видел реже, из-за того что работал с юнит тестированием логики. Но другие типы ошибок все равно есть и встречаются в работе. Уроки, которые описаны в этой статье, не являются всеми возможными уроками, однако они помогли мне уменьшить проблемы при отладке, тестировании и разработке.