Робот не может причинить вред человеку
или своим бездействием допустить,
чтобы человеку был причинён вред.
Айзек Азимов
Я занимаюсь в Яндексе тестированием и хочу рассказать вам об одном нашем экспериментальном проекте. В его рамках мы создали полностью автоматический инструмент для функционального тестирования веб-интерфейсов — Роботестер. Наш робот удовлетворяет первому закону роботехники: он не причиняет вред человеку и не бездействует, когда находит баг, который может создать людям проблемы.
Мы подумали, что сервисом наравне с людьми может пользоваться какое-то количество тестовых роботов, которые оперативно и чётко будут сообщать о его проблемах. Роботы никогда не отдыхают и делают так, чтобы с появившейся ошибкой столкнулось как можно меньше людей.
Нам такая идея очень понравилась, и мы начали её реализовывать.
О функциональном тестировании
Самый простой способ проверить сайт на наличие любого рода багов – дать задание группе людей моделировать поведение пользователей и фиксировать найденные ошибки. Но у этого решения есть недостатки. Например, придётся повторять процесс при каждом релизе и нанимать больше тестировщиков, которые будут выполнять рутинные операции.
Чтобы поставить себя на место высококвалифицированного ручного тестировщика, откройте страницу расширенного поиска Маркета. Начните выбирать себе ноутбук, проверяя при этом, что нигде не съезжают блоки верстки, все элементы страницы подгружаются, а после заполнения формы вы попадаете на страницу со списком ноутбуков, а не получаете 404 ошибку. Такой вид ошибок мы назывем общими. А теперь представьте, что вам нужно проверить на них все сервисы Яндекса. Становится жутко.
Но 404 и в Африке 404, а js ошибка, на каком бы сервисе она ни возникла, – это ошибка, NaN не может быть ни ценой товара, ни расстоянием от пункта А до пункта Б. Этот класс ошибок достаточно однообразен для всех сервисов. Процесс их тестирования можно и нужно автоматизировать, а затем — масштабировать, что сделать с квалифицированными кадрами очень сложно.
Частично справиться с такими проблемами могут автотесты: написав однажды, их можно запускать, когда хочешь. Но автотесты нужно не только писать, но и поддерживать. И мы решили пойти дальше и ручного тестирования, и автотестов.
Робота, который должен сам тестировать сервисы, нужно было научить следующему:
- Обходить сайты как по ссылкам, так и при помощи форм.
- Распознавать элементы страницы и взаимодействовать с ними.
- Генерировать и выполнять тестовые сценарии.
- Отлавливать ошибки.
Обход сайтов
Этот пункт тесно связан с распознаванием элементов: если мы не поймём, что в текстовое поле надо вводить название города, то с большой вероятностью не сможем перейти на следующую страницу и полноценно обойти весь сайт. В тестировании для работы с формами хорошо известны такие технологии, как WebDriver, PhantomJS и т.д.
Стоит сказать, что существует всего один краулер, который умеет обходить ajax-сайты, — CrawlJax. Он оперирует со всеми элементами, но совершенно не разбирается в них, поэтому для хоть немного сложного сайта работает безумно долго. Мы сначала хотели сделать «мозг» для этого робота, но поняли, что его придётся значительно переделывать под наши нужды. Поэтому решили сделать свой продукт.
Мы используем как «статический» краулинг, так и «динамический». В первом случае мы устанавливаем нужные куки (регион, userid) и ходим по ссылкам. Во втором — взаимодействуем с формой, переходим на другую страницу или в другое её состояние, которое получается в результате активации нового поля ввода.
Распознавание элементов страницы
Робот должен моделировать действия пользователя, поэтому ему необходимо взаимодейстовать с элементами веб-страниц также, как это делают люди. Если перед ним, скажем, выпадающий список, то проблем нет, — ему надо выбрать один из вариантов. Также проблем не будет и с checkbox и radio button. Сложности возникают с полями ввода двух типов:
- Сложные поля ввода с JavaScript;
- Поля текстового ввода.
Пока наш робот не умеет взаимодействовать с произвольным полем, которое обрабатывается JavaScript, так как в таком поле может быть «зашита» довольно сложная логика. Нам нужно было научить робота заполнять поля для выбора даты, и мы создали довольно универсальный модуль для взаимодействия с такими полями. Теперь Роботестер умеет выбирать даты на всех страницах Яндекса.
На втором пункте хотелось бы остановиться подробнее. Допустим, перед нами поле текстового ввода. Ясно, что есть поля, куда можно вводить практически любой текст (например, строка поиска на ya.ru). Но во многих случаях ввод произвольного текста приводит к тому, что значительная часть функциональности не используется. Введя некорректные данные, в некоторых случаях робот не сможет не то что перейти на следующую страницу, но и активировать все поля ввода на текущей.
Например, рассмотрим форму поиска рейсов на странице Яндекс.Расписаний. Если по каким-то причинам робот решит, что в первое поле надо вводить какой-то популярный поисковый запрос, а во второе – название определенной профессии, то ничего хорошего из этого не выйдет.
Если в поисковой форме робот совершит подобную ошибку, то мы никогда не увидим (и никогда не проверим) результаты поиска. Если в форме регистрации – никогда не попадем на страницу успешно зарегистрировавшегося пользователя и т.д. Более того, если поля заполнены неправильным образом, в некоторых формах даже не получится нажать кнопку, кликнув по которой челоек должен попасть на новую страницу или увидеть старую в новом состоянии.
Как же робот может понять, что нужно вводить в определённое поле ввода? Подобные задачи возникают также при разработке поисковых роботов, который индексируют deep web. Термин «deep web» используется для обозначения той части информации в интернете, которая «скрыта за формами» — для доступа к ней необходимо заполнить некоторую поисковую форму. Существует несколько подходов к решению этой задачи, прочитать о них можно, например, здесь, здесь или здесь.
Мы расскажем о том, как это решается у нас в тестировании. Но сначала отметим, что хоть и в нашем случае, и в случае с поисковыми роботами, надо определить тип текстового поля, есть важные отличия в наших задачах:
Поисковые роботы обходят все сайты интернета. Эти сайты имеют самую разную вёрстку, и текст на них написан на десятках различных языков. Мы же строим робота только для некоторых страниц Яндекса.
С другой стороны, для поискового робота надо всего лишь максимизировать распознавание полей. Уже какие-то результаты поиска можно будет получить, если в форме будет правильно распознано 90% полей. Но нас не устроит уровень распознавания ниже «абсолютных» 100%. К тому же, мы работаем не только с формами поиска. Следовательно, для того чтобы создать робота, который будет качественно имитировать поведение человека на странице и присылать корректную информацию, нам надо было разработать как можно более точный алгоритм распознавания. Универсальностью мы готовы были пожертвовать.
Наш алгоритм строится на словаре типов. Словарь выглядит следующим образом:
В итоге распознавание типа поля сводится к выбору одного из готовых типов, содержащихся в словаре (на данный момент у нас их 27). Для этого для каждого типа ввода мы сформировали список ключевых слов. В их список были внесены те, которые, присутствуя рядом с данным полем в интерфейсе, повышают вероятность того, что оно относится к данному типу. Разумеется, не все ключевые слова одинаково полезны. Каждому ключевому слову сопоставлен вес, но об этом позже. Алгоритм можно разделить на несколько частей.
Сегментация. Делим интерфейс на части, соответствующие полям ввода.
В нём нас интересует текст и его расположение относительно того поля ввода, которое мы изучаем. Но, во-первых, не очень понятно, что такое «текст» — только видимого текста может быть недостаточно, а сразу весь текст верстки брать плохо, потому что он может содержать в себе много мусора. Поэтому мы используем и тот, и другой текст, но с разными весами.
Задачу извлечения соседнего (видимого или невидимого) текста мы решаем так:
- element.innerHTML+= «@#@$@#!»
- извлекаем (видимый/невидимый) текст из всей формы
- element.innerHTML-= «@#@$@#!»
Дальше находим в полученном тексте подстроку «@#@$@#!». Она и будет символизировать позицию нашего поля ввода в полученном тексте. Текст мы извлекаем при помощи jQuery.
Проставление весов. Полученный текст разбивается на слова, после чего у каждого слова появляется вес. Он зависит от того, видимо слово или нет, и от расстояния между ним и полем ввода.
Выбор типа. Ключевые слова для типа «price» в обилии находятся рядом с полем ввода цены.
Алгоритм оценки релевантности типа выглядит следующим образом. Сначала слова упорядочиваются по весу в убывающем порядке. Назовем этот список списком Х. Теперь будем считать веса уже самих типов полей. У каждого типа есть свой набор слов с весами — это будет список Y. Пересечем этот набор со списком Х и получим список Z. Веса слов в списке Z будут получены перемножением весов в списках X и Y. Дальше нам нужно избавиться от ситуаций, в которых на вес типа влияют «далекие» слова — находящиеся в другой части страницы. Поэтому давайте еще слово в списке Z с номером i разделим на 2i, после чего сложим все веса слов в списке Z – это и будем считать весом данного типа.
Как уже было отмечено, важный элемент этого метода — набор ключевых слов с весами для каждого типа. Веса необходимо выбирать так, чтобы максимальный суммарный вес оказывался у «правильного» типа поля. Как подобрать такие веса? Тут существует множество способов, мы выбрали простую «подгонку». То есть задали веса некоторым способом, а затем корректировали их в ситуации, когда робот неправильно распознавал тип какого-то поля, которое нам было необходимо обработать. Дл обучения робота был создан специальный интерфейс, и, если оператор указывал, что робот ошибочно выбрал тип «Цена», основываясь на слове «от» (а надо было выбирать тип «Адрес» основываясь еще и на слове «маршрут»), то вес слова «от» для типа «Цена» уменьшался, а вес слова «маршрут» для типа «Адрес» увеличивался.
Однако универсальность такого подхода находится на довольно низком уровне. При увеличении числа тестируемых сервисов неизбежно возникнет ситуация, когда мы не сможем вручную подобрать веса, чтобы они работали на всех сайтах. Тогда придет время придумывать что-то новое: например, использовать машинное обучение, основываясь на «правильных» типах полей, заданных оператором для некоторых страниц. Собственно, изначально мы и не думали, что такой простой алгоритм нас устроит, но, как оказалось, он полностью подходит под наши нужды — в данный момент робот успешно распознает все поля всех форм, для которых мы его запускаем. Это 5-6 различных сервисов Яндекса.
О результатах
Роботестер помогает тестировщикам Яндекс.Маркета уже более пяти месяцев. 12% багов, найденных за это время на production среде, принадлежат роботу. Он не спит, не ест (аппаратное время-то он кушает), а главное – он один, но его много. Спасибо 32 ядрам! Хочется верить, что и в тестировании рутинный ручной труд будет заменяться механическим.
Конечно, такие роботы не отнимут труд у ручных тестировщиков и автотестеров, потому что в бизнес-логике и специфике сервиса разобраться бывает сложно даже человеку. Просто в работе квалифицированных кадров станет меньше рутины, а в интернет-сервисах — меньше ошибок.
Во время работы над роботом у нас возникло немало других интересных задач. Например, пришлось придумать свой алгоритм для краулинга, так как за разумное время он не справлялся с обходом мобильного сайта Маркета, содержащего десятки миллионов страниц. Или, например, робот выдавал очень много похожих сообщений об ошибках. Мы разработали специальную систему отчетов, в которых похожие ошибки «склеиваются» в одну. Есть также интересные задачи, до которых пока «не дошли руки» — например, как отличать опечатки в пользовательских комментариях от функциональных ошибок, заметных в тексте. Обо всем этом мы расскажем в следующих сериях.