company_banner

Ещё о тестировании в Яндексе роботами

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

    В идеале именно робот должен проверять, что в результате простой загрузки страницы или сложного взаимодействия с формой ввода не возникает никаких вылетающих наружу исключений, “NaN”, “undefined” или пустых строк на месте подгружаемых данных. Экспериментальный проект по созданию и внедрению такого робота имеет кодовое название “Роботестер”.

    image

    Мы уже рассказывали, как реализовали его и научили работать с формами. Сегодня речь пойдёт о том, как наш робот старается найти максимальный объем функциональности сервиса, а затем и «понять» его.

    Когда-то каждый сайт представлял собой статическую страницу, контент которой формировался с помощью одного запроса к серверу. Сейчас же многие сайты можно отнести к категории Rich Internet Applications (RIA): они стали намного интерактивнее и их содержимое меняется в зависимости от поведения пользователя на странице. Веб-приложения отличаются от традиционных сайтов использованием таких технологий, как Javascript и AJAX, которые позволяют значительно менять вид и содержимое приложения без изменения адреса в браузере.

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

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

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

    Конечно, бывают как устаревшие сайты, где почти нет разумных элементов (там фактически сработает только первый краулер), так и, наоборот, современные RIA, где url страницы может вообще не меняться (тогда будет работать только второй краулер). Тем не менее, у нас получилась довольно гибкая система, которую легко настроить практически для любого сервиса.

    Как устроен краулер страниц


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


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

    Предположим, граф страниц нашего сайта имеет вид, представленный на рисунке:

    image

    Пусть каждое ребро графа — это переход по ссылке. Такой инструмент, как CrawlJax, начинает идти с одной вершины всеми возможными путями. Этот способ обхода сайта может работать очень долго. Мы же решили, что будем действовать по-другому: отберем “точки входа” в графе (зеленые вершины на рисунке) таким образом, чтобы в идеале набор зеленых вершин стал 1-сетью. Как правило, в этом случае точки входа — это страницы с формами, а доступные на расстоянии 1 страницы — это результат поиска. Таким образом, для тестирования сайта нам достаточно найти точки входа, выполнить на них различные действия и проверить на ошибки страницы, на которые мы перешли. Стоит отметить, что большинство страниц 1-окрестности какой-либо точки входа идентичны по функциональности, поэтому тестировать можно не всех представителей такой окрестности.

    Итак, чтобы протестировать сайт, робот должен иметь список некоторых его страниц.

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

    • Должна быть возможность осуществлять краулинг за разумное время.
    • Краулер должен отбирать как можно более отличающиеся по структуре страницы.
    • Количество страниц, которые нужно тестировать, на выходе должно быть ограничено.
    • Краулер должен собирать со страницы даже те ссылки, которые загружаются динамически (изначально не содержатся в коде страницы).
    • Должна быть возможность поддерживать авторизацию, регионозависимость и другие настройки сервисов Яндекса.

    Требование №1: время
    Бывает необходимо контролировать длительность процесса тестирования. Допустим, необходимо выкатить релиз и хочется быстро проверить основную функциональность. Длительность краулинга можно поставить в некоторые рамки при помощи ограничения на глубину и количество посещенных страниц.

    Требование №2: покрытие
    Второе требование вытекает из необходимости покрыть тестами как можно большую функциональность за как можно меньшее время. Очевидно, что на контент-сервисе есть классы экивалентных (по функциональности) страниц. Кластеризовать такие страницы можно, ориентируясь на URL страницы и ее html-код. Мы разработали способ, позволяющий быстро и достаточно качественно разделять такие страницы на кластеры.

    Мы используем два типа расстояний между страницами:

    • UrlDistance отвечает за различие урлов. Если длина двух урлов одинаковая, то расстояние между урлами обратно пропорционально количеству совпавших символов. Если длина разная, то расстояние пропорционально разности длин.
    • TagDistance отвечает за различие html-кода страниц. Расстояние между двумя DOM-деревьями — разность количества одноименных тегов.

    Стоит обратить внимание, что реализация второго требования должна быть согласована с первым требованием. Допустим, нам нужно отобрать страницы для тестирования сайта Яндекс.Маркет. Предположим, с каждой страницы ведет N ссылок и нас интересует глубина краулинга K. Чтобы сравнить все страницы, нам придется загрузить html-код О(NK) страниц. На это может уйти немало времени, учитывая, что ссылок, ведущих с каждой страницы контентного сайта, может быть 20-30. Такой способ нам не подходит. Будем фильтровать страницы не в самом конце, а на каждом шаге.

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

    • Первый этап фильтрации: сравниваем урлы по UrlDistance, оставляя L наиболее отличающихся.
    • Второй этап фильтрации: загружаем html-код L наиболее отличающихся по метрике UrlDistance страниц и сраниваем их при помощи TagDistance.

    Таким образом, нам нужно загрузить код только для O(L·K) страниц. При всех данных упрощениях процесса фильтрации страниц, качество получающихся «точек входа» нас устраивает!

    Требование №3: количество тестируемых страниц
    Реализация третьего требования позволит нам ограничивать количество страниц, на которых будут проведены тесты. Грубо говоря, мы говорим роботу «отбери N самых отличающихся по функциональности страниц и протестируй их».

    Требование №4: учитывать динамически загружаемый контент
    Собрать ссылки даже с блоков, загружаемых динамически, и переходить только по визуально доступным ссылкам (моделирование поведения человека) позволяет WebDriver.

    Требование №5: контекст
    Чтобы учитывать контекст вроде региона или прав пользователя (различная выдача, различный функционал), краулер должен уметь работать с куками. Например, вы знали, что даже главная страница Яндекса в разных регионах может быть различной?

    Итак, мы выполнили все указанные требования и получили возможность быстро и эффективно находить как можно больший объем функциональности сайта. Робот нашел «точки входа». Теперь необходимо посетить 1-окрестность, или, проще говоря, провзоимодействовать с формой.

    Краулер форм


    Чтобы протестировать функционал сайта, нужно уметь им воспользоваться. Если начать бездумно кликать на всё подряд и вводить любые значения в текстовые поля ввода (как вводить не любые) то качественно протестировать форму за разумное время не получится. Например, с такими формами на Яндекс.Директе, как на рисунке ниже, взаимодействовать достаточно сложно.

    image

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

    Давайте формализуем задачу. Можно считать, что все поля ввода — это выпадающие списки. В самом деле, поле ввода текста для нас фактически становится выпадающим списком, когда мы определили его тип и выбрали конечное количество значений, которые можем в него ввести. Наша задача — протестировать форму оптимальным образом, что подразумевает максимальное покрытие при фиксированном количестве тест-кейсов или минимальное количество тест-кейсов при заданном покрытии (в зависимости от того, что нам важнее — скорость тестирования, или уровень покрытия).

    Простой случай. Полей ввода фиксированное количество, и они не изменяются, то есть не зависят друг от друга. Данная задача достаточно популярна и широко освещена. Мы, например, используем программу с красивым названием Jenny.

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

    image

    Число — это номер поля ввода, буква — номер варианта, который нужно выбрать. Покрытие степени N означает, что если зафиксировать любые N столбцов, то в них встретятся все возможные комбинации.

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

    • Удаляются поля ввода достаточно редко, поэтому такой случай мы не рассматриваем.
    • Если при заполении поля A изменилось поле B, то нужно произвести топологическую сортировку полей ввода, получив порядок, при котором мы сначала будем заполнять А, а потом — B, если B зависит от А. На практике мы считаем, что вышестоящие (имеено по физическому расположению на странице) элементы не зависят от нижестоящих.
    • Если в результате взаимодействия появилось новое поле ввода, то мы считаем, что оно зависит только от одного из предыдущих полей ввода. В действительности это не так, однако это допущение существенно упрощает задачу.

    Переход в другую вершину осуществляется посредством изменения состояния одного поля ввода.

    Допустим, изначально форма состоит из элементов A1...An. Для каждого Ai выполним:

    • Пробуем повзаимодействовать с Ai всеми возможными способами. Пусть этих способов k штук.
    • Зафиксируем те элементы Bi1...Bik, которые появлялись при различных заполнениях Ai.
    • Рекурсивно повторить процедуру для каждого из Bij.

    При таком подходе алгоритм может работать очень долго, поскольку количество способов взаимодействия будет расти экспоненциально от числа элементов. Для того чтобы краулинг формы мог закончился в обозримом будущем, перебор всех возможных способов взаимодействия осуществляется только для элементов, находящихся на глубине не более 2, а для всех остальных выбираем одно случайное значение. Такой подход необходим для тестирования форм, у которых необходимо заполнение некоторого количества обязательных полей.

    Алгоритм


    Допустим, изначально форма состоит из элементов A1...An. Для каждого Ai выполним:

    1. Пробуем повзаимодействовать с Ai всеми возможными способами. Пусть этих способов k штук.
    2. Зафиксируем те элементы Bi1...Bik, которые появлялись при различных заполнениях Ai.
    3. Для каждого элемента Bij, пока не дойдем до конца заполнения формы, выполним:
    3.1. Провзаимодейтсвуем с элементом Bij всеми возможными способами.
    3.2. С каждым элементом, который становится доступен при заполнении Bij, взаимодействуем единственным случайным образом.

    image

    Оптимальное покрытие генерируется из расчета, что Аi — выпадающий список, у которого вариантов столько, сколько из него путей до уровня 3.

    Приведем конкретный пример.

    image

    Из вершины A1 пять путей до уровня 3, из вершины A2 — четыре и из вершины A3 тоже четыре. Соответственно, можно представить А1 как выпадающий список с пятью полями, а А2 и А3 — с четырьмя. Следовательно запускать Jenny робот будет с аргументами «5 4 4».

    Мы научили робота находить функционал сервиса и бездумно (относительно человека) пользоваться им. Конечно, не стоит ожидать, что в ближайшее время он зарегистрируется в Яндекс.Директе, самостоятельно создаст рекламную компанию для себя любимого и прославится на весь мир, хотя он уже умеет создавать проекты на внутреннем сервисе (правда они, как ни странно, пока не выстрелили :)). Однако мы от него этого и не ждем! Он заполняет формы грамотно и до конца. Мы ждем от него качественного тестирования веб-интерфейсов, и эти наши ожидания он оправдывает.
    Яндекс
    407.34
    Как мы делаем Яндекс
    Share post

    Comments 13

      +12
      Слава роботам! :)
      image

      Ежели что, это Opera Developer 18.0 на хромиуме.
        0
        Спасибо! Я передал ответственным людям.
        К сожалению, пока еще не все проверяется роботами.
          +1
          На самом деле пока я заливал картинку и открывал для себя вопиющее отсутствие катов в камментах хабра, Опера предложила обновиться. В порядке эксперимента попробовал повторить скриншот — не смог.

          Роботы бдят! :)
            0
            и уже выпустил апдейт!
              0
              В комментариях есть спойлеры: <spoiler>.
          • UFO just landed and posted this here
              0
              Карты пытаются загрузить скрипт по адресу
              api-maps.yandex.ru/2.0.33/release/combine.xml?modules=4z4y8x2:5A5R3)3^623536985D3_4]9c5,3s3i5y@d5z4L343y7j7o7n7k7l535052515{5)5(5]5[5*5!595^5@6*3*683U4(3{6Z3[379$999!935C4d4_4K6b3$3o6(6^3b3=8Y2~3,5J5G5H8V4A6f4h774I6L3Z3w4-5f4.4{8w5a8C4=4)4,![5h!_!=!{5l4`4[4}5e5d8~!H6s6u6r8f!D4Y4542!V!Z!Y!27,@Q3Q9b8n7`7~8c8e8d433d3l9e8[8{32314X7(3h4W3a3W3X2-3p3O3e2`3v3x3t3u63898!888684838Z8782858X7C!a!b!c3q5=3r5-5_5}9i9k5$!k!g!f!i5855545657@S5b!d!e8R8Q8U7H!~!:@b$s$r$t$H$p$n@-$o$y9)694*4j6K!*3@974e3!3n3.3(8(4g6k3}3Y8D8B8z4~8y!]5m5g6Y5k9f4J!,4s4O8I7L9g!1309d@49s6t9F6F6w9I6I6v9z9G6D6x443`@J@I@K3P8i8g8h8l8k8j7-7{8r!U7]7G8m!T7J7_8p8a!X7I7}8o7:!W7=@W@R!L!z!E8`3c9a3k3M@U909Z919Y3N5V8q8b7w46334Z7[!v5U5L5N5M5Q9h8,8_8)3g8]5S6{6=3]@i7*7$786S6O6P4R4S!K5o4b3R4V7580815B4l7f677Z9m7R4G!l!j9j4Q4M4N4:4B4C4F7S!h6T6U6Q6R!O@X@Z@Y@1@2@V@T5r5x7M7O5v8O8S5u8L5s5w8J5t8N7p7E@C@E7K!`@a@c$J$K$Y$Z$6$8$!$V$U$T$S$($5$3$4$_${$X$]$9$[$A@=$w1S4i!!!7944f5F9*964T798A!}4r7b7h8H!C6_6}7t@D9.@B9:7y7z4o@m@39y9M9E9U9W9Q9u9L9T9X9v6z6y6B9J6E9V41407v7m9_9~7A7r9=7B7q9-38@0!N!B!F5P7T4v5T6J4a5O!84U!$8u8s3m5K@g6[8-6-3V4t7@7^6~5j!.5i8=4p6:4q66658:763j8*8^8@7P7N3~7i7Q4E@M@L6V926,!P5I$m8T8K8W8P7F5q$z$v9]9[231^0*1B2y011H0Q5E$)2U2[1Y$x0K7!6)4n7d3:7x7u9,9`7U5Z$e$E@@@7$b$a@8@5$f@!@9@69O@A@w6A9B@v@s@u6H6G4m9t9w9x9C7.7D7s2P!M@}$k$l@_@{@H3-4c47!5!34u648E4H!p4P728$!Q8M$L$1$79{2t0x2e2d0l0k2j2u2w2D29050N0X0b0m$}162x0U2M0~1}$d$g0y2c1w2V22251d@:0$0R1A@x@z9N9S6C9R9D9K0T1e2A2i271i@(@G!(484@!)4^@j8F8.4D4x73!n7V1(2r2f0O1Q$@$^$*$$$R0^0E1R0t1z2v0c1K2^061n2L0.0V2*1b1q1g0I0q0i1h0g1!2}$C$c0e0]9P@y0Z6$4k!-712z2W2h0@2g1F1O2T262m2Z0!1V172b027W9^951r0-2]1{0B2I2J0S1[0p1`1N7X&jsonp_prefix=ymaps2_0_33
              У меня прокси на Java он падает на этом адресе говорит «URISyntaxException»
              Если параметр modules пропустить через urlencode то все работает
              где-то вы RFC нарушили
                0
                Спасибо, мы посмотрим!
                  0
                  Опять сломалось :(
                  напишите себе юнит-тест вида
                  new java.net.URI(xxxx);
                  new java.net.URL(xxxx);
                  


                    0
                    Говорят, исправление будет в новой версии API
                0
                Как на счет открыть API для Роботестера, только надо запретить парочке сайтов:
                Disallow: google.com
                Disallow: bing.com
                Возможно это сделает популярнее Яндекс среди буржуйских разработчиков.
                  0
                  Да, была такая мысль (да и осталась), но пока все не настолько стабильно работает, чтобы выдавать  API внешним людям. Но мы думаем «в эту сторону», да.
                  0
                  Не совсем понятен пример с простым случаем задачи минимизации тестов при заданной степени покрытия. Может, я заблуждаюсь, но мне кажется, что, если вторую строчку «1b 2a 3a» убрать, то все равно останется покрытие степени 2.

                  Only users with full accounts can post comments. Log in, please.