Недавно наткнулся на ошибку в Android приложении Яндекс.Метро. Если бы был чемпионкой мира по синхронному плаванию, то обязательно спросил бы: «Кто создавал программу „для галочки“? Кто работал „на отшибись“? Кто слабое звено?». Недоумение вызывала не сама ошибка, а то, что она попала в приложение и всё ещё не исправлена.
В данной статье опишу простые советы, которые помогут улучшить качество программ. Проиллюстировать их смогу с помощью ошибок приложения Яндекс.Метро. Надеюсь, что это окажется полезным как разработчикам и тестировщикам, так и руководителям проектов.
Начать хотелось бы с главы «Изучение знаменитых (и не очень знаменитых) ошибок» из книги «Наука отладки». Если ещё не читали, то рекомендую прочитать. В этой главе описаны «глупые» ошибки и стечения обстоятельств, которые стоили жизней и миллионы долларов. Все эти ошибки объединяло то, что выявляющий их тестовый сценарий было чрезвычайно сложно воспроизвести, в результате многие проверки были просто пропущены.
В статье же мы будем рассматривать распространённые приложения, которые протестировать может любой участник команды. Давайте проведём небольшой эксперимент. Если у вас нет Android устройства, то попросите минут на десять у коллег или друзей. Скачайте приложение Яндекс.Метро и попробуйте его протестировать. Интересует актуальная на текущий момент версия 1.63 от 02.11.2012 сборка 159 (на Google Play стоит дата 21.01.2013). Для корректности проверки предлагаю снять галочку «Автообновление» в настройках Google Play.
Проверять будем основную функцию: нахождение маршрута между двумя станциями метрополитена Москвы. Посмотрите на карту метро. Подумайте, что следовало бы протестировать в приложении, что могло бы быть неправильно реализовано. Явной ошибкой будет сообщение «Маршруты не найдены», что некорректно, поскольку нет изолированных станций.
Эта же ошибка может проявить себя не так явно:
Уверен, что многие из вас нашли ошибку. Давайте попробуем разобраться, откуда она появилась. Упростим процесс разработки до трёх ролей: постановщик задачи, разработчик и тестировщик. На каком этапе добавили ошибку? Мне сложно представить постановщика, который скажет, что если маршруты не найдены, то надо выдать соответствующее сообщение. Тестировщик, скорее всего, мог только попросить заменить одно сообщение другим, но не добавить логику. Остаётся разработчик.
Сообщение гласит, что маршруты не найдены, а, значит, работа алгоритма завершилась неудачей. Вот тут для разработчика должен был быть первый сигнал. Нельзя просто так отмахиваться от невозможного кода. Надо либо попытаться его проверить, либо настроить обратную связь. Но, допускаю, что данная ошибка была штатной в ходе проверок, когда карта линий метро была несвязной.
1. Инварианты
Существует такое понятие инвариант цикла. С его помощью можно доказать корректность циклов. Многие ошибки выявляются сразу.
Вместо проверки инвариантов некоторые программисты пытаются «раскрутить» цикл в голове: «у нас расстояние равно десяти, здесь уменьшим на две станции, но добавим пересадку, в результате на одну станцию станем ближе...». Главная ошибка в том, что при таком подходе проверяется конкретный путь, а не формулируются инварианты.
Чтобы разобраться, что это за понятие, посмотрите, например, книгу, которую написал А. Шень. Программирование: теоремы и задачи (ссылка взята отсюда). Пусть это и пособие для занятий в школе, но на простом языке объясняются и решаются как простые, так и довольно сложные задачи.
2. Проверка алгоритма
Предположим, что алгоритм написан. Для простоты будем считать, что реализован в виде некоторой функции. В ходе реализации были тупиковые ветви, в которые никогда не должны были попасть. Как проверить алгоритм?
Во-первых, надо попробовать позапускать функцию хоть с какими-нибудь корректными параметрами. Среди корректных параметров выбрать граничные и изломы.
Во-вторых, можно попробовать «раскручивать» исключительные ситуации, чтобы понять, при каких параметрах повышаются шансы проявления исключения.
В-третьих, оцените множество исходных параметров. Возможно, что получится провести полное тестирование в рамках какого-нибудь блока. Если вернуться к задаче с метро, то достаточно проверить всего 188 * 188 = 35344 вариантов. Это может быть сделано очень быстро. После проверки алгоритм всё ещё может не находить оптимальные маршруты, но он точно не будет падать в исключения.
3. Исключения
Иногда разработчикам необходимо обработать ошибки, причём ошибки не являются штатными, то есть их вообще не должно было бы быть. Приведу пример из жизни. Один раз CD диск разрушился в дисководе. Система не могла понять в чём дело, поскольку диск был, потом дисковод не открывался, а сейчас диска нет. В результате была выдана ошибка: «Возникла непредусмотренная ошибка». Ценности от подобного сообщения нет ни пользователю, ни разработчику. Для пользователя нет подсказки, что же дальше делать, а разработчику не ясно, в чём же была причина. Если выдаёте невозможное исключение пользователю, то постарайтесь добавить хоть какую-нибудь информацию, которая поможет разобраться в ситуации.
Использование встроенного в язык программирования механизма исключений очень удобно. Достаточно всего одной строки, чтобы сообщить об ошибке, при этом нет необходимости каждый раз проверять результат функции. Если функция может выдать ошибку, то просто сгенерируйте исключение. Часто можно встретить игнорирование исключений. Возникло исключение, его отловили и продолжили работу. Потом где-то возникнет ошибка, но о её причинах никто не узнает. Например, вместо сообщения «файл не найден» будет выдана ошибка «некорректный файл конфигурации». Перехватывать следует только те исключения, которые обработаете сами или распространите выше.
4. Обратная связь
От ошибок никто не застрахован, поэтому необходимо предусмотреть, что делать, если ошибка всё-таки возникнет. Проще всего передать основную информацию о входных параметрах на сайт программы. Можно перед этим попросить пользователя осуществить подобную передачу. Если посмотреть на разрешения программы Яндекс.Метро, то одно из них — это «Неограниченный доступ к Интернету». Почему бы при ошибке не сообщить о паре проблемных станций?
Предположим, что у вас честное офлайн приложение, в этом случае можете предложить пользователю отправить email (в том числе автоматически) или просто открыть страницу в браузере. Даже если нет возможности передать все необходимые параметры через адресную строку, то закодируйте ошибки в виде чисел. У вас хотя бы будет статистика возникновения ошибок и их места в программе.
Точно не следует отправлять на сайт, где необходима регистрация. Вам нужно узнать об ошибке, а не пользователю сообщить о ней. Как исключение можете предложить компенсацию (в том числе виртуальную) за найденные ошибки.
Если предлагаете заполнить форму об ошибке, то дайте возможность пользователю скопировать уже готовый текст с описанием ошибки. Во-первых, ему это намного проще, чем придумывать и набирать текст, а на телефоне это не очень-то и удобно. Во-вторых, вы сможете включить важную информацию. Иначе может появиться сообщение: «Сколько раз запускаю — не работает!». Конечно, у пользователя всегда есть возможность поставить низкую оценку в магазине приложений и оставить отзыв, но это не самый лучший выход.
Если ничего не предпринимать, то можно подождать подробный отзыв об ошибке на Хабрахабре или упоминание на одном из WTF сайтов.
5. Простые и стандартные алгоритмы
Когда разрабатываете программы, то старайтесь создавать максимально простые алгоритмы, чтобы минимизировать количество возможных ошибок. Оптимизировать можно будет позже, но быстрый ошибочный алгоритм всегда хуже правильного, но пока ещё медленного.
Второй момент — это использование стандартных алгоритмов. Если есть возможность, то используйте уже готовые реализации. Категорически нельзя использовать свои алгоритмы криптозащиты, возьмите стандартные реализации. Ценой ошибки может быть серьёзная уязвимость, например, почитайте про WPS.
6. Тестирование
Многие разработчики не любят тестировать свои программы. Часто тестирование ограничивается предложенным в постановке примером или какими-нибудь простыми вариантами. Чаще всего причин всего две. Во-первых, разработчик же сам написал этот код и ошибок там быть не должно. Фактически, тестирование будет означать увеличение срока разработки, а разработчик же хороший и может это сделать быстрее. Иногда забываются какие-нибудь константы, временные настройки прямо в коде, фиксированный язык и т.п.
Во-вторых, разработчику важно ощущение, что цель достигнута. Он неделю пытается заставить двигаться этот объект по экрану, компилироваться код и т.п. Теперь что-то заработало. Самое сложное позади, и у разработчика нет желания возобновлять поиск ошибок. Сейчас он — молодец, поскольку программа запустилась и что-то делает. Если завтра сообщат о множестве ошибок, то они будут легко исправлены, и это будет очередная победа. Если же разработчик сразу потратит ещё день на поиск и исправление ошибок, то приза за это не получит. Проблема в том, что тестировщик тоже может пропустить ошибку, а времени на поиск ему может понадобится значительно больше.
Про отсутствие тестировщиков хорошо написано у Joel Spolsky.
7. Начинаем тестирование
Что следует проверять при тестировании, на что обращать внимание? Во-первых, надо проверить стандарные случаи. Например, для карты метро возьмём две станции на одной линии, но на противоположных лучах относительно кольцевой и проверим, что предлагается путь по прямой без пересадок. Дальше можно взять две пересекающиеся линии и проверить, что вариант с одной пересадкой попал в результаты. Дальше проверить, что если линии не пересекаются, то программа находит маршрут с двумя пересадками.
Потом желательно проверить граничные варианты, например, программа корректно обрабатывает ситуацию, когда пользователь пытается найти маршрут между одной и той же станцией и между станциями, соединёнными переходом. На возражение, что никто не стал бы прокладывать маршрут между соседними станциями отвечу, что в программе предусмотрен ввод станций по названию, а гость города может и не знать о нахождении станций на карте.
Затем следует посмотреть на условие и смысл задачи и выполнить проверку в критических точках («изломах»). Если посмотреть на карту метро Москвы, то среди всех станций выделяются четыре группы:
- (Новокузнецкая, Третьяковская) — для некоторых комбинаций для пересадки достаточно лишь пересечь платформу;
- (Китай-Город) — аналогично предыдущей группе;
- (Охотный ряд, Площадь революции, Театральная) — переход между станциями «Охотный ряд» и «Площадь революции» возможен только через станцию «Театральная»;
- (Александровский сад, Арбатская, Библиотека имени Ленина, Боровицкая) — переход между станциями «Александровский сад» и «Боровицкая» возможен либо через станцию «Арбатская», либо через станцию «Библиотека имени Ленина».
При проверке данных узлов оказывается, что при необходимости транзита программа выдаёт некорректный результат. Маршруты: «Охотный ряд» — «Площадь революции» и «Александровский сад» — «Боровицкая».
В ходе тестирования рекомендуется ещё выполнить дополнительные проверки, в результате которых должны быть получены все предусмотренные сообщения программы.
8. Ошибка найдена
Когда ошибка найдена, то следует обязательно проверить все смежные варианты. Возможно, что ошибка типична для множества случаев.
В Яндекс.Метро программа отказывается искать маршрут между станциями «Арбатская» (голубая) и «Боровицкая», а маршрут между станциями «Смоленская» (голубая) и «Боровицкая» прокладывается через станцию «Киевская». Если расширить область поиска, то обнаружится, что программа корректно прокладывает маршрут между станциями «Смоленская» (голубая) и «Чеховская».
Ещё можно посмотреть на найденные варианты маршрутов между станциями «Маяковская» и «Марьино», также между станциями «Тверская» и «Марьино». Можно увидеть некоторое несоответствие, которое показывает различную обработку станций в алгоритме, когда те оказываются на узлах пересадок. Можно убедиться, что не всегда самый быстрый маршрут (по мнению программы) оказывается первым в списке.
«He won’t stand beating»
В заголовок решил вынести цитату из «Alice in Wonderland». В мультфильме на фразу Алисы об убийстве времени ответили, что «он этого не любит».
Если в программе используется время, то необходимо аккуратно разрабатывать программу и тестировать её. Недавно была представлена ошибка 29.02.2013 на сайте РЖД.
Когда говорим о времени, то надо помнить, что бывают различные часовые пояса. Если когда-нибудь назначали конференции с кем-то из других стран, то без явного указания часового пояса могли возникнуть проблемы. Если некорректно настроено клиентское ПО, то при создании встречи тоже могут возникать ошибки, например, мне приходит уведомление о встрече, причём ошибка из-за летнего времени может быть в час. Если пишите какую-нибудь дату и время в письме, то обязательно напишите точный часовой пояс. В программах обязательно храните часовой пояс рядом с временем или храните всё в одном и том же часовом поясе.
Буквально пару дней назад наткнулся на ошибку в поисковой системе Wolfram Alpha. На определённый круг вопросов она даёт нужный ответ. Примеры подобных запросов и областей знаний можно посмотреть, например, на About.com
Ошибка в поисковой системе предельна проста: она до сих пор не знает, что часовой пояс Москвы (MSK) изменился и соответствует UTC+4. В результате все запросы о времени для России оказываются некорректными. Об ошибке им написал, об ответе сообщу.
Из-за чего возникла подобная ошибка? Скорее всего, что программа просто использует некоторую таблицу с временными поясами, причём об её обновлении никто не подумал. Может быть и подумали, но на обработку подобной ситуации не стали тратить время. Возможно, что проблема в зимнем времени.
Когда речь идёт о времени, то любой тестировщик должен проверить високосные года, а также переход на летнее и зимнее время. При этом обращаю внимание, что весной одного часа просто не будет (в два часа ночи сразу будет три часа летнего времени), а осенью интервал между двумя и тремя часами номи будет пройден два раза (один раз по летнему времени и один раз по зимнему времени). Зададим Wolfram Alpha вопрос о разнице между двумя моментами времени. Система корректно вычла один час. Вы это учитываете в программах?
Ещё надо посмотреть на поведение программы на границе полуночи, когда сравнения по времени уже не работают (23:59 и 00:01), а даты стали отличаться. Что будет, если начать в программе некоторую операцию сегодня, а продолжить уже завтра? Когда разрабатываете программу, работающую со временем, то следует быть очень внимательными.
Когда используются номера недель, то надо обязательно их тоже проверять. Дело в том, что если первое января попадает на понедельник, вторник или среду, то первой будет считаться следующая неделя. Несколько лет назад ошибочное определение недель встречал в телефонах Nokia. Подобную ошибку иногда допускают дизайнеры при проектировании календарей.
Со временем связана ещё проблема — персонализация часового пояса. Представьте себе, что я нахожусь в другом поясе относительно сервера, в этом случае, очевидно, я хочу видеть время в своём часовом поясе. Если я часто путешествую, то в каждой поездке мне приходится сравнивать время не только с сервером, но и с сохранённым в настройках. Часто в настройках форумов и сайтов предлагается выбрать часовой пояс и выбор начинается с GMT-12. Причём нигде нет простого варианта «мой текущий часовой пояс». Если я сменю часы в операционной системе, то было бы неплохо видеть время в соответствующем часовом поясе. Единственной проблемой тут будет невозможность сразу определить, о каком часовом поясе идёт речь.
Думайте о пользователях
Если пишите программу лишь «для галочки», чтобы просто была, то дальше можете уже не читать. Когда создаёте программу, то должны понимать, для кого именно её делаете, какие задачи она должна решать и т.п. Задача не просто выпустить продукт, а создать продукт под конкретные категории пользователей. Попробуйте задать вопрос, чего ещё может не хватать данному конкретному пользователю.
Вернёмся к Яндекс.Метро. В программе добавлено уведомление, что метро закрыто в определённый интервал времени. Оно срабатывает при запуске программы или смене карты. Но за сессию срабатывает лишь однажды. Главный минус в том, что если программа уже была запущена ранее, то при прокладке маршрута пользователь не сможет узнать, закрыто ли метро. Это одно из замечаний. С другой стороны, программу часто используют не только после входа в метро, но и заранее, чтобы запланировать поездку. Почему бы не добавить предупреждение о закрытии метро за пару часов? Дополнительно можно добавить справочную страницу, где указать, например, время работы и стоимость проезда.
Что касается времени работы метро, то даже такая простейшая функция была реализована с ошибкой. Предположим, что кто-то приехал из Москвы в Киев или Минск. Открывает программу и получает сообщение, что метро закрыто. Но ещё уйма времени до закрытия. Дело в том, что наступила полночь в Москве, а программа даже и не думает проверить часовой пояс, а лишь ориентируется на текущее время:
Что ещё в метро зависит от текущего времени (кроме открытия и закрытия)? Во-первых, это интервалы между поездами и время ожидания. Об этом поговорим чуть позже. Во-вторых, это возможные перекрытия станций или переходов из-за ремонта эскалаторов. Если учесть, что информация о ремонте эскалаторов и изменении режимов работы станций обновляется довольно редко, то было бы очень неплохо, чтобы она отображалась на карте и учитывалась при прокладке маршрута. Дополнительно она может быть просто описана на вкладке «Новости».
На времени ожидания поездов остановлюсь отдельно. Если посмотреть описание онлайн-версии Яндекс.Метро, то там написано:
3. Какое именно время считается и почему оно приблизительное?
Считается время проезда между станциями, время перехода с линии на линию. Не учитывается время спуска или подъема по эскалатору на начальной и конечной станциях. Для переходов и проездов берется среднее время, и по разным причинам оно может на пару минут отличаться от реального времени (например, поезд прибыл не вовремя или на переходе скопилось много пассажиров).7. Откуда берутся данные для расчета маршрутов?
Для расчета маршрутов используются данные программы MMetro © Константин Штенников.
Если кто-то не видел программу MMetro, то в ней при подборе маршрута можно задать режим учёта времени ожидания: не учитывать, дневной или вечерний и раннее утро. Можете проверить, что в седьмом вопросе корректно написано, что онлайн-версия Яндекс.Метро в качестве времени перехода и времени проезда между двумя станциями берёт данные MMetro. Время ожидания поезда вообще не учитывается. То есть «поезд прибыл не вовремя» из третьего пункта означает, что «вы пришли, а поезда нет». Длительное время ожидания по вечерам требует прокладки маршрутов с минимумом пересадок. С учётом этого прослеживается явное лукавство во втором ответе: «На схеме появится предлагаемый нами маршрут, а на панели справа от схемы будет подробно описано, как нужно ехать и сколько времени займет поездка». Правильнее написать про «оптимистический маршрут» или честно про «маршрут без времени ожидания».
В Android версии Яндекс.Метро используются те же данные. Приведу отзыв пользователя Ami Meio от 2 Января 2013 года: «Новые станции метро открыты уже 5й день, а на схеме их до сих пор нет( да и про время поездки программа всегда врет.» Описание в Google Play гласит:
Поиск оптимального маршрута в метро.Приложение не требует подключения к интернету — поэтому вы можете пользоваться им непосредственно во время поездки. Планируйте подземные маршруты в Москве, Санкт-Петербурге, Киеве, Харькове и Минске. Узнайте время поездки с учетом пересадок. А еще Метро подскажет, в какую часть поезда лучше садиться, чтобы доехать быстрее.
В мобильном приложении Яндекс.Метро есть:
— автоматическое построение оптимального маршрута;
— расчёт времени в пути;
— пополнение проездного билета «Подорожник» (Санкт-Петербург);
— а также, если ваш телефон поддерживает NFC (модели Galaxy Nexus, Google Nexus S, Sony Xperia S и HTC One X), то с помощью приложения вы можете узнать, сколько поездок осталось на одноразовой карточке московского метрополитена.
На объявленную корректность расчёта времени в пути уже посмотрели. Это, конечно, мелочи, но приложение — это не только apk-файл, но и описание на сайте. У меня взгляд зацепился за следующее: (а) отсутствие пробела после первого предложения; (б) во втором предложении слово Интернет написано со строчной буквы; (в) во втором же предложении не ясно, чем можно пользоваться во время поездки: приложением или Интернетом; (г) вначале слова пишутся без использования буквы «ё» («учетом», «еще»), а потом буква «ё» появляется в слове «расчёт»; (д) в последнем предложении написано «московского метрополитена».
Не совсем понятно как соотносится фраза «Приложение не требует подключения к интернету» с требуемыми разрешениями:
Неограниченный доступ к Интернету
Приложение сможет создавать сетевые сокеты и использовать различные сетевые протоколы. Так как браузер и другие приложения самостоятельно реализуют функции отправки данных в Интернет, это разрешение предоставлять не обязательно.
Подключение/отключение сети Wi-Fi
Приложение сможет подключаться к точкам доступа Wi-Fi и отключаться от них, а также изменять конфигурацию сетей Wi-Fi на устройстве.
Изменение сетевых настроек
Приложение сможет изменять состояние подключения к сети.
Чаще всего запускаю Яндекс.Метро, чтобы узнать, в какую часть поезда следует садиться, чтобы выйти ближе к нужному переходу или выходу. Это очень хорошая идея. К сожалению, к реализации есть замечания. Во-первых, довольно сложно определить конкретный номер вагона на основе изображения. Могу предположить, что пять вагонов обозначают: первый вагон, начало поезда, центр, окончание поезда и последний вагон. Возможно, что чуть более наглядно было бы добавить стрелку в сторону перехода, то есть направление движения после выхода из вагона. Но основная проблема с выходами из вагонов в том, что на многих станциях стоят ограждения, из-за которых выход из первого вагона превращается в обход заграждения через центр зала. Пытался быстрее доехать, а тут, наоброт, теряю время. Почему бы не скорректировать настройки?
При поездке по определённому адресу одной из проблем становится определение нужного выхода. Если выход единственный, то Яндекс.Метро поможет. Если же выходов несколько, то поможет программа 2ГИС. Она умеет прокладывать маршрут с учётом поездок на метро. Но заключительный штрих оказался смазан. Посмотрите на скриншот. Нам надо добраться до станции метро Тульская, но нас ведут к центру станции, а не к одному из входов на станцию (обозначены буквами «М» слева вверху и внизу). С выходами из метро ситуация аналогичная. Будьте бдительны!
Одна из тенденций последнего времени — это создание программ без описания. Вы получаете нечто и не знаете, что оно делает. Считается, что интерфейс должен быть понятным. Почему бы не дать дополнительную информацию об объектах программы?
Единственный доступный в программе «информационный» документ — это лицензионное соглашение. У меня возник вопрос по поводу пунктов 5.1 и 5.2:
5.1. Пользователь настоящим уведомлен и соглашается, что при использовании в Программе функции «Определение местоположения», Программа получает данные о координатах местоположения Пользователя и передает их Правообладателю для отображения ближайшей к Пользователю станции метро, до момента отключения указанной функции.
5.2. Пользователь может в любой момент отказаться от передачи данных, указанных в п. 5.1, отключив соответствующую функцию.
Стандартная функция Android «определение местоположения» у меня выключена и приложение Карты отказывается определить моё местоположение. В настройках Яндекс.Метро никакой дополнительной опции нет.
Но Яндекс.Метро продолжает находить ближайшую станцию метро. То есть идёт нарушение лицензионного соглашения со стороны Яндекса (или просто враньё)? Аналогичная ситуация с соглашением по программе Яндекс.Карты, где о местоположении сказано лишь в пункте 5.3:
5.3. Пользователь настоящим уведомлен и соглашается, что при включении входящей в состав Программы функции «Сообщать о пробках» Программа передает Правообладателю обезличенные данные о точном местонахождении и параметрах движения Пользователя, полученные от GPS-устройства, или о приблизительном местонахождении Пользователя, определённые по активной соте оператора связи, sim-карта которого в данный момент используется в мобильном устройстве Пользователя, для сбора статистической информации о состоянии дорожного движения, до момента отключения указанной функции.
Но даже при отключённой функции «Сообщать о пробках» программа продолжает показывать текущее местонахождение (стандартная функция Android всё ещё выключена).
Прикрываясь с помощью «AS-IS» компании могут безответственно выпускать программные продукты любого качества. Разгильдяйство не наказывается пока «пипл хавает», можно делать всё «на отшибись».
Заключение
Достаточно «включить мозг» при разработке и тестировании, чтобы улучшить программный продукт. Постарайтесь подумать как пользователь, поставьте себя на его место. Если не получается, то найдите типичного пользователя и поговорите с ним. Ощущать желания клиента должен не только постановщик задачи, но и вся команда. Качественный продукт дарит радость не только пользователю, но и вам. Надеюсь, что данная статья принесёт пользу.
Автор: Boriso