Автор Анна Евкова
Преподаватель который помогает студентам и школьникам в учёбе.

Тестирование производительности программ: подходы в зависимости от категорий приложений

Содержание:

Введение:

Целью тестирования производительности программы является выявление "проблем" тестируемого объекта в соотстветствии с выбранным механизмом тестирования.

Под проблемами понимаются любые отличия от эталонного (ожидаемого) поведения программы.

Основных способов тестирования всего три:

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

2) Стресс тестирование - в этом сценарии приложение тестируют с целью получения информации о его "пределах". В случае сетевого приложения это может быть получение максимального rps (requests per second), максимального кол-ва открытых сокетов, транзакций с базой данных и т.п.

В случае локального приложения стресс тестирование проводится на его внутренних компонентах, например в случае мобильного приложения по просмотру видео, приложение может быть нагружено с помощью видео в разрешении 4k с кодеком h265 без аппаратного ускорения с целью проверки работоспособности приложения в этих условиях.

3) Тестирование стабильности - этот сценарий направлен на долгосрочную эксплуатацию приложения под ожидаемой нагрузкой с целью проверки его работоспособности в долгосрочной перспективе.

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

В случае если приложение написано на языке с автоматическим управлением памяти (garbage collector, "умные" указатели и т.п.) то в основном это тестирование используется для поиска утечек ресурсов и контроля освобождаемых ресурсов. Так же, в некоторых случаях (java, go) этот сценарий используется в совокупности со стресс тестированием для предсказания поведения сборщика мусора (и так называемой "остановки мира" для очистки памяти) в подобных ситуациях.

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

В настоящее время принято разделять приложения на серверные и клиентские. Отличие между ними достаточно простое: "серверным" приложением является то, которое по ожидаемому сценарию своей работы должно обслуживать одного или более "удалённых" клиентов.

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

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

А клиентским является пользовательский интерфейс, отображаемый в браузере или frontend.

В своей работе я намерен отдельно рассмотреть тестирование двух категорий приложений:

Клиент-серверные приложения на примере работы веб сайта с традиционным REST API, mysql 5.7, nginx 1.15.8 и golang 1.11.5.

Клиентского приложения на примере мобильного приложения "Калькулятор" написанным на Flutter 1.2.1 (stable).

Глава 1. Клиент серверное приложение.

Нагрузочное тестирование:

Основным критерием производительности серверного приложения (бакенда веб сайта) является его способность обрабатывать клиентские запросы за "комфортное время". Подчеркну данную формулировку, разные запросы выполняются за разное время и фронтенд должен это учитывать.

Пример на основе нашего REST API:

/v1/orders/list

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

Однако комфортное время выполнения следующего запроса:

/v1/orders/place

может быть значительно больше, что должно быть в свою очередь отображено на фронтенде. (спиннер, прогресс бар, зависит от фантазии дизайнера).

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

Для начала нашего тестирования определим типичный сценарий поведения фронтенда:

1) Запрос на /v1/orders/list - для отображения текущих заказов пользователя.

2) Запрос на /v1/goods/list - для отображения возможных товаров, которые может захотеть приобрести пользователь.

3.a) Запрос на /v1/orders/place - для добавления товара в список заказов.

3.b) Повторный запрос на /v1/goods/list с целью получения второй страницы списка товаров, обновления фильтра поиска, etc.

Процедура авторизации опущена, т.к. не представляет интереса в тестировании.

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

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

Как пример фреймворка, отлично реализующего поставленную задачу я приведу пример: Locust (https://github.com/locustio/locust).

Однако, представим, что при этом нам нужно знать, что увидит клиент, если возникает отказ бакенда?

Для решения подобной проблемы есть разряд программного обеспечения, который кратко можно описать как "headless browser". Самый распространённый это, без сомнений, Headless Chrome/Chromium, однако заточенным под автоматизацию сценариев тестирования является Selenium (https://www.seleniumhq.org/).

С использованием указанных выше инструментов мы без труда сможем нагрузить наш бакенд (и проверить фронтенд) на работоспособность в ожидаемой ежедневной нагрузке, постепенно увеличивая её аналогично со статистикой посещаемости сайта.

Стресс тестирование:

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

Безусловным лидером в подобных видах тестирования является TCP Kali (https://github.com/satori-com/tcpkali).

Допустим, нас интересует как много пользователей в единый момент времени может обслужить веб сайт?

Для ответа на этот вопрос мы не обязаны (хотя можем) эмулировать полное поведение пользователя, достаточно просто открытия соединения и запрос на любой из запросов (включая картинку с изображением товара, фронтенд скрипты и т.д.). И именно это и делает Kali, позволяя нам выяснить лимиты нашего nginx/golang сервиса.

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

Пример:

После запуска tcpkali на нашем тестовом веб сервисе мы не смогли справится со знаменитой "C10k problem" или проблема 10 тысяч одновременных соединений, поскольку в стандартной поставке конфигурация nginx не подразумевает использование более 1 ядра и алгоритм асинхронной отправки файлов (kqueue) не был корректно оптимизирован под ресурсы сервера.

После исправления указанных проблем и повторного запуска tcpkali, мы смогли приодолеть указанный лимит и следующей нашей проблемой стал запрос /v1/orders/place, который делает запрос к базе данных даже если клиент не добавляет какой-либо товар.

Более тонкая настройка tcpkali под наши методы, разумеется, способна выявлять и дальше узкие места, однако общий подход к стресс тестированию остаётся неизменным.

Остаётся он неизменным и в независимости от используемого по: так, мы вольны использовать кластер из Selenium для проведения того же тестирования, однако это потребует гораздо большего кол-ва ресурсов.

И будет правильным указать прошлого лидера в области стресс тестирования: ПО Siege (https://www.joedog.org/siege-home/), Jmeter от разработчиков Apache (https://jmeter.apache.org/).

Тем не менее, подход к стресс тестированию, его сценарий и логика остаются неизменными.

Тестирование стабильности:

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

Но так же мы должны протестировать наше приложение на его стабильную работу в течении долгого времени.

Зачастую, после стресс тестирования и удовлетворения от долгой работы по оптимизации проекта разработчики считают, что тестирование на стабильность является избыточным, что разумеется не верно. Одна из причин: Закон Мерфи в разработке ПО работает превосходно и сочетание таких факторов, как остановка мира сборщиком мусора + бэкап базы данных в неудачное время способны привести к отказу бакенда при казалось бы нулевой нагрузке.

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

1) Настройка разнообразных метрик для контроля состояния сервера, таких как например, свободное место на дисках, текущий LA, текущий rps, время обработки разных типов запросов, кол-во клиентов и многие, многие другие метрики.

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

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

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

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

Тестирование на проникновение:

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

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

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

Так же, к подобным тестам можно отнести тесты на DDoS (Distributed Denial of Service), которые мы уже частично провели с помощью tcpkali на этапе стресс тестирования. Однако, возможные атаки включают в себя не только огромное кол-во стандартных запросов пользователя, но и специфические атаки, такие как SYN flood и наш веб сервис должен быть готов к этому.

К используемым утилитам для этого типа тестирования можно указать:

1) Nessus (https://www.tenable.com/products/nessus/nessus-professional) - лучший инструмент в данный момент на рынке ПО для автоматизированного тестирования на проникновение, обязателен к использованию.

2) w3af (http://w3af.org/) - второй по качеству инструмент для подобного тестирования, имеет меньший функционал в отличие от Nessus, однако в нём более удобная поддержка модулей, предназначенная для настройки инструмента под особенности вашего проекта.

Глава 2. Клиентское приложение.

Нагрузочное тестирование:

В качестве нашего тестового приложения мы рассмотрим Калькулятор с минимальным функционалом, написанный с помощью фреймворка Flutter на языке Dart и запускаемый на платформе Android 6.0.1.

Основными инструментами тестирования в этом случае будут фреймворки, предоставляемые компанией Google, такие как Espresso & AndroidX.

В данном случае сценарий тестирования будет достаточно простым:

1) Имплементация действий пользователя в UI приложения, в том числе некорректных, повторяющихся, нажатия на близко находящиеся кнопки и т.п.

2) Имплементация некорректных вводных данных от пользователя, таких как деление на 0.

3) Поведение приложения на нестандартных конфигурациях, разных версиях Android.

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

Стресс тестирование:

Используя указанные выше фреймворки мы так же можем эмулировать "стрессовое" состояние приложение, такие как:

1) Введение заведомо превышающих возможную точность стандартных типов dart чисел, в том числе с плавающей точкой.

2) Выполнение заведомо неверных операций, включая, но не ограничивающиеся, делением на 0, математические операции вызывающие исключения и операции, которые не могут быть выполнены используя текущую математическую библиотеку, используемую в приложении.

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

Как следствие, с помощью этого тестирования мы стараемся отсечь граничные ситуации, которые могут привести к падению нашего приложения.

Тестирование на стабильность:

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

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

Тестирование на проникновение:

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

В случае, если приложение реализовано некорректно, в случае попадания apk к злоумышленнику, он через деобфускацию получит доступ к токену для логина и как следствие получит доступ уже к админке сайта.

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

В этом случае автоматизированное тестирование может быть выполнено с помощью DeGuard (https://apk-deguard.com/) и написания самостоятельных тестов с помощью вашего любимого языка.

Вывод:

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

Так же данные подходы к тестированию приложений подходят ко всем остальным подкатегориям (мобильные игры, тонкие клиенты, операционные системы, драйвера и т.п.) при смене некоторых утилит.

Например, в случае тестирования драйвера для bluetooth 4.2 ресивера можно использовать подход из клиент - серверного типа приложения, однако средства тестирования будут заменены на gdb/pprof и возможно tcpdump.

Используемые ресурсы:

https://en.wikipedia.org/

https://github.com/locustio/locust

https://www.seleniumhq.org/

https://github.com/satori-com/tcpkali

https://www.joedog.org/siege-home/

https://jmeter.apache.org/

https://www.tenable.com/products/nessus/nessus-professional

http://w3af.org/

https://apk-deguard.com/