![](https://habrastorage.org/webt/23/xo/vg/23xovgngq7wo4uy6rdybalbvpda.png)
В начале этого года Тензор проводил митап в городе Иваново, на котором я выступил с докладом про эксперименты с фаззинг-тестированием интерфейса. Тут расшифровка этого доклада.
Когда обезьяны заменят всех QA? Можно ли отказаться от ручного тестирования и UI автотестов, заменив их фаззингом? Как будет выглядеть полная диаграмма состояний и переходов для простого TODO приложения? Пример реализации и о том, как работает такой фаззинг далее под катом.
Всем привет! Меня зовут Докучаев Сергей. Последние 7 лет я занимаюсь тестированием во всех его проявлениях в компании Тензор.
![](https://habrastorage.org/webt/ya/jf/zr/yajfzrmghqw9vrl2nsei2goz0yi.png)
У нас более 400 человек отвечают за качество выпускаемых продуктов. 60 из них занимаются автоматизацией, тестированием безопасности и производительности. Для того, чтобы поддерживать десятки тысяч E2E тестов, контролировать показатели производительности сотен страниц и выявлять уязвимости в промышленном масштабе — нужно применять инструменты и методы, проверенные временем и ни раз испытанные в бою.
![](https://habrastorage.org/webt/kx/e8/l4/kxe8l447ky_zjtnxsx4wlv9_cm4.png)
И, как правило, на конференциях рассказывают именно о таких кейсах. Но кроме этого существует много всего интересного, что в промышленных масштабах пока сложно применить. Вот про это интересное и поговорим.
![](https://habrastorage.org/webt/eu/a2/9l/eua29lpxw4duvwbtfsouz2nnqre.png)
В фильме «Матрица» в одной из сцен Морфеус предлагает Нео выбрать красную или синюю таблетку. Томас Андерсон работал программистом и мы помним какой выбор он сделал. Будь он отъявленным тестировщиком — слопал бы обе таблетки, чтобы посмотреть, как система поведёт себя в нестандартных условиях.
Комбинировать ручное тестирование и автотесты стало практически стандартом. Разработчики лучше всего знают как устроен их код и пишут юнит-тесты, функциональные тестировщики проверяют новый или часто меняющийся функционал, а весь регресс уходит к разнообразным автотестам.
Однако, в создании и поддержке автотестов внезапно не так много авто- и довольно много ручной работы:
- Нужно придумать что и как протестировать.
- Нужно найти элементы на странице, вбить нужные локаторы в Page Objects.
- Написать и отладить код.
- При любых изменениях — актуализировать сценарий. Причём если функционал/интерфейс очень часто меняются, то автотесты оказываются не у дел, а ROI стремится к нулю.
![](https://habrastorage.org/webt/2n/21/sm/2n21smohvxvzgctmjds89ajmxrc.png)
К счастью, в мире тестирования существует ни две и не три таблетки. А целая россыпь: манки-тестирование, формальные методы, фаззинг-тестирование, решения на основе AI. И ещё больше их комбинаций.
![](https://habrastorage.org/webt/m4/-o/pi/m4-opiuhjqngdgixzabmmtwhtyy.gif)
Утверждение, что любая обезьяна, которая будет бесконечно долго печатать на пишущей машинке, сможет напечатать любой наперёд заданный текст, прижилось и в тестировании. Звучит неплохо, можем заставить одну программу бесконечно кликать по экрану в рандомных местах и в конечном итоге сможем найти все ошибки.
![](https://habrastorage.org/webt/4t/_b/2r/4t_b2ri0ahzdbx1pth1j3m0jqs4.png)
Допустим сделали мы такой TODO и хотим его проверить. Берём подходящий сервис или инструмент и видим обезьянок в действии:
![](https://habrastorage.org/webt/ah/6u/qq/ah6uqqsxt1gzdifxy0qnf4c2wmy.png)
По такому же принципу мой кот как-то, полежав на клавиатуре, безвозвратно сломал презентацию и её пришлось делать заново:
![](https://habrastorage.org/webt/ro/ew/2a/roew2aelrahu7cm-qnnzyo1gz-8.png)
Удобно, когда после 10 действий приложение выбрасывает исключение. Тут наша обезьянка сразу понимает, что произошла ошибка, а мы по логам можем понять хотя бы приблизительно, как она повторяется. А что если ошибка произошла после 100К случайных кликов и выглядит как валидный ответ? Единственным значимым плюсом этого подхода является максимальная простота — ткнул кнопку и готово.
![](https://habrastorage.org/webt/wd/6i/ff/wd6iff6nhrhjqlyrv8ynp8efdma.png)
Противоположностью такого подхода являются формальные методы.
![](https://habrastorage.org/webt/js/-w/zv/js-wzvt53r8g6htfob6uyj7kr6a.png)
Это фотография Нью-Йорка в 2003 году. Одно из самых ярких и многолюдных мест на планете, Таймс-сквер, освещают только фары, проезжающих мимо машин. В тот год миллионы жителей Канады и США на три дня оказались в каменном веке из-за каскадного отключения электростанций. Одной из ключевых причин произошедшего оказалась race condition ошибка в ПО.
Критичные к ошибкам системы требуют особого подхода. Методы, которые опираются не на интуицию и навыки, а на математику называют формальными. И в отличии от тестирования они позволяют доказать, что в коде отсутствуют ошибки. Создавать модели гораздо сложнее, чем писать код, который они призваны проверить. А их использование больше похоже на доказательство теоремы на лекции по матанализу.
![](https://habrastorage.org/webt/fc/sb/sw/fcsbsw4f8zb-j-8qmm9-9do4g6e.png)
На слайде часть модели алгоритма двух рукопожатий, написанной на языке TLA+. Думаю для всех очевидно, что использование этих инструментов при проверке формочек на сайте сравни постройке Боинга 787 для проверки аэродинамических свойств кукурузника.
![](https://habrastorage.org/webt/v3/p1/vj/v3p1vjsvth38pavf9zcsm6obuek.png)
Даже в традиционно критичных к ошибкам медицинской, аэрокосмической и банковской отраслях очень редко прибегают к такому способу тестирования. Но сам подход незаменим, если цена любой ошибки исчисляется миллионами долларов или человеческими жизнями.
Фаззинг тестирование сейчас чаще всего рассматривается в контексте тестирования безопасности. И типовую схему, демонстрирующую такой подход, возьмём из OWASP гайда:
![](https://habrastorage.org/webt/tl/5f/2s/tl5f2sc5flfvnk6720jowovrzmc.png)
Тут у нас есть сайт, который требуется протестировать, есть БД с тестовыми данными и инструменты, при помощи которых будем отправлять указанные данные на сайт. Вектора представляют из себя обычные строки, которые были получены опытным путём. Такие строки с наибольшей вероятностью могут привести к обнаружению уязвимости. Это как та кавычка, которую многие на автомате ставят на место числа в URL из адресной строки.
![](https://habrastorage.org/webt/de/ns/xi/densxikytnxqgmqx07rxpkhrkky.png)
В простейшем случае у нас есть сервис, который принимает запросы и браузер, который их отправляет. Рассмотрим кейс с изменением даты рождения пользователя.
![](https://habrastorage.org/webt/uk/cc/gr/ukccgrfxppfzdjf7llguxmgvieu.png)
Пользователь вводит новую дату и нажимает кнопку “Сохранить”. На сервер улетает запрос, с данными в json формате.
![](https://habrastorage.org/webt/6d/kp/5a/6dkp5a2nrnp2kspql41g5xpyyr8.png)
И если всё хорошо, то сервис отвечает двухсотым кодом.
![](https://habrastorage.org/webt/l3/1i/85/l31i85xgjr_5vm7cnd9c0f8_wqy.png)
С json’ами удобно работать программно и мы можем научить наш инструмент для фаззинга находить и определять даты в передаваемых данных. И он начнёт подставлять различные значения вместо них, например будет передавать несуществующий месяц.
![](https://habrastorage.org/webt/lz/25/tk/lz25tkg2lz77n7xwhb5gw1pdjly.png)
И если мы в ответ вместо сообщения о невалидной дате получили исключение, то фиксируем ошибку.
Фаззить API несложная задача. Вот у нас передаваемые параметры в json’е, вот мы отправляем запрос, получаем ответ и анализируем его. А как быть с GUI?
Вновь рассмотрим программу из примера про манки-тестирование. В ней можно добавлять новые задачи, отмечать выполненные, удалять и просматривать корзину.
![](https://habrastorage.org/webt/4t/_b/2r/4t_b2ri0ahzdbx1pth1j3m0jqs4.png)
Если заняться декомпозицией, то мы увидим, что интерфейс — это не единый монолит, он тоже состоит из отдельных элементов:
![](https://habrastorage.org/webt/aa/lu/d7/aalud7m-erxrevfxsxphx7wddoc.png)
С каждым из контролов мы можем сделать не так-то и много. У нас есть мышка с двумя кнопками, колёсиком и клавиатура. Можно кликать по элементу, наводить на него курсор мыши, в текстовые поля можно вводить текст.
Если мы введём в текстовое поле какой-то текст и нажмём Enter, то наша страница перейдёт из одного состояния в другое:
![](https://habrastorage.org/webt/lj/yy/0b/ljyy0bwbqfcwofg4q-wii1ef0js.png)
Схематически это можно изобразить вот так:
![](https://habrastorage.org/webt/7s/vc/zs/7svczswx_wsdegnc969l7yn5rfc.png)
Из этого состояния мы можем перейти в третье добавив ещё одну задачу в список:
![](https://habrastorage.org/webt/8w/f5/yz/8wf5yz6t1zsfukkn39phttwc-2o.png)
А можем удалить добавленную задачу, вернувшись в первое состояние:
![](https://habrastorage.org/webt/24/et/hg/24ethgz0sv9kn8lccrkcqub1kq8.png)
Или кликнуть по надписи TODOs и остаться во втором состоянии:
![](https://habrastorage.org/webt/tm/w0/2f/tmw02fjrt58fcdx7azkhuezqtzo.png)
А теперь попробуем реализовать Proof-of-Concept такого подхода.
![](https://habrastorage.org/webt/sc/r0/od/scr0oda4buldnwokkkek6fvq8te.png)
Для работы с браузером возьмём chromedriver, работать с диаграммой состояний и переходов будем через python библиотеку NetworkX, а рисовать будем через yEd.
![](https://habrastorage.org/webt/ni/hr/dx/nihrdx6nb9mqwa8xj_5ongveiqk.png)
Запускаем браузер, создаём инстанс графа, в котором между двумя вершинами может быть множество связей с разной направленностью. И открываем наше приложение.
![](https://habrastorage.org/webt/kf/wz/pq/kfwzpqeh6khdlstssmktxdkaur0.png)
Теперь мы должны описать состояние приложения. Из-за алгоритма сжатия изображения, мы можем использовать размер картинки в формате PNG как идентификатор состояния и через метод __eq__ реализовать сравнение этого состояния с другими. Через атрибут iterated мы фиксируем, что были прокликаны все кнопки, введены значения во все поля в этом состоянии, чтобы исключить повторную обработку.
![](https://habrastorage.org/webt/_w/op/ua/_wopuae6lclnoojttupbsyz4f9k.png)
Пишем основной алгоритм, который будет обходить всё приложение. Тут мы фиксируем первое состояние в графе, в цикле прокликиваем все элементы в этом состоянии и фиксируем образующиеся состояния. Далее выбираем следующее не обработанное состояние и повторяем действия.
![](https://habrastorage.org/webt/db/fm/tg/dbfmtgzjyy_rv5zgwy1oz4lkodw.png)
При фаззинге текущего состояния мы должны каждый раз возвращаться в это состояние из нового. Для этого мы используем функцию nx.shortest_path, которая вернёт список элементов, которые нужно прокликать, чтобы перейти из базового состояния в текущее.
Для того, чтобы дождаться окончания реакции приложения на наши действий в функции wait используется Network Long Task API, показывающий занят ли JS какой-либо работой.
Вернёмся к нашему приложению. Исходное состояние имеет следующий вид:
![](https://habrastorage.org/webt/cy/y8/rr/cyy8rrbwpwpmmixtiuklpx127j8.png)
После десяти итераций по приложению мы получим такую диаграмму состояний и переходов:
![](https://habrastorage.org/webt/xo/xk/j_/xoxkj_36wizfkcs80tpchpgzvh0.png)
Через 22 итерации вот такой вид:
![](https://habrastorage.org/webt/wk/oq/yl/wkoqylhvzqawp9eakeohgkwcjfk.png)
Если же запустить наш скрипт на несколько часов, то он внезапно сообщит, что обошёл все возможные состояния, получив следующую диаграмму:
![](https://habrastorage.org/webt/zu/wf/an/zuwfangui38kmoobidm9nukcnc4.png)
Так, с простым демонстрационным приложением мы справились. А что будет, если натравить этот скрипт на реальное веб-приложение. А будет хаос:
![](https://habrastorage.org/webt/-y/bt/ae/-ybtaeidyefc7c378poteqndmte.gif)
Мало того, что на бэкенде происходят изменения, сама по себе страница постоянно перерисывается при реакции на таймеры или события, при выполнении одних и тех же действий мы можем получить разные состояния. Но даже в таких приложениях можно найти куски функционала, с которыми наш скрипт может справиться без значительных доработок.
Возьмём для испытаний страницу аутентификации СБИС:
![](https://habrastorage.org/webt/zn/gh/sf/znghsfmq_hyvy64lkw46iifwsqu.png)
И для неё достаточно быстро получилось построить полную диаграмму состояний и переходов:
![](https://habrastorage.org/webt/dd/de/z4/dddez4pbauiwjmbn69vkpf7gyka.png)
![](https://habrastorage.org/webt/ch/7n/8m/ch7n8mo4hdqqsv1kz1yizdqhvyc.png)
Отлично! Теперь мы можем обходить все состояния приложения. И чисто в теории найти все ошибки, которые зависят от действий. Но как научить программу понимать, что перед ней ошибка?
В тестировании ответы программы всегда сравниваются с неким эталоном, называемым оракулом. Им может быть ТЗ, макеты, аналоги программы, прошлые версии, опыт тестировщика, формальные требования, тест-кейсы и т.д. Часть этих оракулов мы также можем использовать в нашем инструменте.
![](https://habrastorage.org/webt/ys/hf/tq/yshftqc8s0tworeez9dysqrjla8.png)
Рассмотрим последний паттерн “а раньше было по-другому”. Автотесты ведь регрессионным тестированием занимаются.
Вернёмся к графу после 10 итерации по TODO:
![](https://habrastorage.org/webt/_t/av/qm/_tavqmbuanfwhmrp88dcr-mrfxu.png)
Сломаем код, который отвечает за открытие корзины и вновь прогоним 10 итераций:
![](https://habrastorage.org/webt/ul/er/av/uleravnc-co73kmnjwq4pu2xzay.png)
А далее сравним два графа и найдём разницу в состояниях:
![](https://habrastorage.org/webt/7e/j7/2t/7ej72tlr1ckznlwnwwh5tg9ywty.png)
Можем подвести итог для данного подхода:
![](https://habrastorage.org/webt/r0/o5/wl/r0o5wl9tq1twhnriyaresfvqmvs.png)
В текущем виде этот приём можно использовать для тестирования небольшого приложения и выявления очевидных или регрессионных ошибок. Для того, чтобы методика взлетела для больших приложений с нестабильным GUI потребуются значительные доработки.
Весь исходный код и список использованных материалов можно найти в репозитории: https://github.com/svdokuchaev/venom. Тем, кто хочет разобраться с применением фаззинга в тестировании, очень рекомендую The Fuzzing Book. Там в одной из частей описан такой же подход к фаззингу простых html форм.
![](https://habrastorage.org/webt/k9/xa/nr/k9xanrcapby134hbjkav-7g2868.png)