[Из песочницы] Фаззинг тестирование веб-интерфейса. Расшифровка доклада

23xovgngq7wo4uy6rdybalbvpda.png

В начале этого года Тензор проводил митап в городе Иваново, на котором я выступил с докладом про эксперименты с фаззинг-тестированием интерфейса. Тут расшифровка этого доклада.

Когда обезьяны заменят всех QA? Можно ли отказаться от ручного тестирования и UI автотестов, заменив их фаззингом? Как будет выглядеть полная диаграмма состояний и переходов для простого TODO приложения? Пример реализации и о том, как работает такой фаззинг далее под катом.
Всем привет! Меня зовут Докучаев Сергей. Последние 7 лет я занимаюсь тестированием во всех его проявлениях в компании Тензор.

yajfzrmghqw9vrl2nsei2goz0yi.png

У нас более 400 человек отвечают за качество выпускаемых продуктов. 60 из них занимаются автоматизацией, тестированием безопасности и производительности. Для того, чтобы поддерживать десятки тысяч E2E тестов, контролировать показатели производительности сотен страниц и выявлять уязвимости в промышленном масштабе — нужно применять инструменты и методы, проверенные временем и ни раз испытанные в бою.

kxe8l447ky_zjtnxsx4wlv9_cm4.png

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

eua29lpxw4duvwbtfsouz2nnqre.png

В фильме «Матрица» в одной из сцен Морфеус предлагает Нео выбрать красную или синюю таблетку. Томас Андерсон работал программистом и мы помним какой выбор он сделал. Будь он отъявленным тестировщиком — слопал бы обе таблетки, чтобы посмотреть, как система поведёт себя в нестандартных условиях.

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

Однако, в создании и поддержке автотестов внезапно не так много авто- и довольно много ручной работы:

  1. Нужно придумать что и как протестировать.
  2. Нужно найти элементы на странице, вбить нужные локаторы в Page Objects.
  3. Написать и отладить код.
  4. При любых изменениях — актуализировать сценарий. Причём если функционал/интерфейс очень часто меняются, то автотесты оказываются не у дел, а ROI стремится к нулю.


2n21smohvxvzgctmjds89ajmxrc.png

К счастью, в мире тестирования существует ни две и не три таблетки. А целая россыпь: манки-тестирование, формальные методы, фаззинг-тестирование, решения на основе AI. И ещё больше их комбинаций.

m4-opiuhjqngdgixzabmmtwhtyy.gif

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

4t_b2ri0ahzdbx1pth1j3m0jqs4.png

Допустим сделали мы такой TODO и хотим его проверить. Берём подходящий сервис или инструмент и видим обезьянок в действии:

ah6uqqsxt1gzdifxy0qnf4c2wmy.png

По такому же принципу мой кот как-то, полежав на клавиатуре, безвозвратно сломал презентацию и её пришлось делать заново:

roew2aelrahu7cm-qnnzyo1gz-8.png

Удобно, когда после 10 действий приложение выбрасывает исключение. Тут наша обезьянка сразу понимает, что произошла ошибка, а мы по логам можем понять хотя бы приблизительно, как она повторяется. А что если ошибка произошла после 100К случайных кликов и выглядит как валидный ответ? Единственным значимым плюсом этого подхода является максимальная простота — ткнул кнопку и готово.

wd6iff6nhrhjqlyrv8ynp8efdma.png

Противоположностью такого подхода являются формальные методы.

js-wzvt53r8g6htfob6uyj7kr6a.png

Это фотография Нью-Йорка в 2003 году. Одно из самых ярких и многолюдных мест на планете, Таймс-сквер, освещают только фары, проезжающих мимо машин. В тот год миллионы жителей Канады и США на три дня оказались в каменном веке из-за каскадного отключения электростанций. Одной из ключевых причин произошедшего оказалась race condition ошибка в ПО.

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

fcsbsw4f8zb-j-8qmm9-9do4g6e.png

На слайде часть модели алгоритма двух рукопожатий, написанной на языке TLA+. Думаю для всех очевидно, что использование этих инструментов при проверке формочек на сайте сравни постройке Боинга 787 для проверки аэродинамических свойств кукурузника.

v3p1vjsvth38pavf9zcsm6obuek.png

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

Фаззинг тестирование сейчас чаще всего рассматривается в контексте тестирования безопасности. И типовую схему, демонстрирующую такой подход, возьмём из OWASP гайда:

tl5f2sc5flfvnk6720jowovrzmc.png

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

densxikytnxqgmqx07rxpkhrkky.png

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

ukccgrfxppfzdjf7llguxmgvieu.png

Пользователь вводит новую дату и нажимает кнопку «Сохранить». На сервер улетает запрос, с данными в json формате.

6dkp5a2nrnp2kspql41g5xpyyr8.png

И если всё хорошо, то сервис отвечает двухсотым кодом.

l31i85xgjr_5vm7cnd9c0f8_wqy.png

С json«ами удобно работать программно и мы можем научить наш инструмент для фаззинга находить и определять даты в передаваемых данных. И он начнёт подставлять различные значения вместо них, например будет передавать несуществующий месяц.

lz25tkg2lz77n7xwhb5gw1pdjly.png

И если мы в ответ вместо сообщения о невалидной дате получили исключение, то фиксируем ошибку.

Фаззить API несложная задача. Вот у нас передаваемые параметры в json«е, вот мы отправляем запрос, получаем ответ и анализируем его. А как быть с GUI?

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

4t_b2ri0ahzdbx1pth1j3m0jqs4.png

Если заняться декомпозицией, то мы увидим, что интерфейс — это не единый монолит, он тоже состоит из отдельных элементов:

aalud7m-erxrevfxsxphx7wddoc.png

С каждым из контролов мы можем сделать не так-то и много. У нас есть мышка с двумя кнопками, колёсиком и клавиатура. Можно кликать по элементу, наводить на него курсор мыши, в текстовые поля можно вводить текст.

Если мы введём в текстовое поле какой-то текст и нажмём Enter, то наша страница перейдёт из одного состояния в другое:

ljyy0bwbqfcwofg4q-wii1ef0js.png

Схематически это можно изобразить вот так:

7svczswx_wsdegnc969l7yn5rfc.png

Из этого состояния мы можем перейти в третье добавив ещё одну задачу в список:

8wf5yz6t1zsfukkn39phttwc-2o.png

А можем удалить добавленную задачу, вернувшись в первое состояние:

24ethgz0sv9kn8lccrkcqub1kq8.png

Или кликнуть по надписи TODOs и остаться во втором состоянии:

tmw02fjrt58fcdx7azkhuezqtzo.png

А теперь попробуем реализовать Proof-of-Concept такого подхода.

scr0oda4buldnwokkkek6fvq8te.png

Для работы с браузером возьмём chromedriver, работать с диаграммой состояний и переходов будем через python библиотеку NetworkX, а рисовать будем через yEd.

nihrdx6nb9mqwa8xj_5ongveiqk.png

Запускаем браузер, создаём инстанс графа, в котором между двумя вершинами может быть множество связей с разной направленностью. И открываем наше приложение.

kfwzpqeh6khdlstssmktxdkaur0.png

Теперь мы должны описать состояние приложения. Из-за алгоритма сжатия изображения, мы можем использовать размер картинки в формате PNG как идентификатор состояния и через метод __eq__ реализовать сравнение этого состояния с другими. Через атрибут iterated мы фиксируем, что были прокликаны все кнопки, введены значения во все поля в этом состоянии, чтобы исключить повторную обработку.

_wopuae6lclnoojttupbsyz4f9k.png

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

dbfmtgzjyy_rv5zgwy1oz4lkodw.png

При фаззинге текущего состояния мы должны каждый раз возвращаться в это состояние из нового. Для этого мы используем функцию nx.shortest_path, которая вернёт список элементов, которые нужно прокликать, чтобы перейти из базового состояния в текущее.
Для того, чтобы дождаться окончания реакции приложения на наши действий в функции wait используется Network Long Task API, показывающий занят ли JS какой-либо работой.

Вернёмся к нашему приложению. Исходное состояние имеет следующий вид:

cyy8rrbwpwpmmixtiuklpx127j8.png

После десяти итераций по приложению мы получим такую диаграмму состояний и переходов:

xoxkj_36wizfkcs80tpchpgzvh0.png

Через 22 итерации вот такой вид:

wkoqylhvzqawp9eakeohgkwcjfk.png

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

zuwfangui38kmoobidm9nukcnc4.png

Так, с простым демонстрационным приложением мы справились. А что будет, если натравить этот скрипт на реальное веб-приложение. А будет хаос:

-ybtaeidyefc7c378poteqndmte.gif

Мало того, что на бэкенде происходят изменения, сама по себе страница постоянно перерисывается при реакции на таймеры или события, при выполнении одних и тех же действий мы можем получить разные состояния. Но даже в таких приложениях можно найти куски функционала, с которыми наш скрипт может справиться без значительных доработок.
Возьмём для испытаний страницу аутентификации СБИС:

znghsfmq_hyvy64lkw46iifwsqu.png

И для неё достаточно быстро получилось построить полную диаграмму состояний и переходов:

dddez4pbauiwjmbn69vkpf7gyka.png

ch7n8mo4hdqqsv1kz1yizdqhvyc.png

Отлично! Теперь мы можем обходить все состояния приложения. И чисто в теории найти все ошибки, которые зависят от действий. Но как научить программу понимать, что перед ней ошибка?

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

yshftqc8s0tworeez9dysqrjla8.png

Рассмотрим последний паттерн «а раньше было по-другому». Автотесты ведь регрессионным тестированием занимаются.

Вернёмся к графу после 10 итерации по TODO:

_tavqmbuanfwhmrp88dcr-mrfxu.png

Сломаем код, который отвечает за открытие корзины и вновь прогоним 10 итераций:

uleravnc-co73kmnjwq4pu2xzay.png

А далее сравним два графа и найдём разницу в состояниях:

7ej72tlr1ckznlwnwwh5tg9ywty.png

Можем подвести итог для данного подхода:

r0o5wl9tq1twhnriyaresfvqmvs.png

В текущем виде этот приём можно использовать для тестирования небольшого приложения и выявления очевидных или регрессионных ошибок. Для того, чтобы методика взлетела для больших приложений с нестабильным GUI потребуются значительные доработки.
Весь исходный код и список использованных материалов можно найти в репозитории: https://github.com/svdokuchaev/venom. Тем, кто хочет разобраться с применением фаззинга в тестировании, очень рекомендую The Fuzzing Book. Там в одной из частей описан такой же подход к фаззингу простых html форм.

k9xanrcapby134hbjkav-7g2868.png

© Habrahabr.ru