Автоматизация тестирования приложений является важным элементов в обеспечении процессов CI/CD. В этой статье мы поговорим о практическом использовании инструмента с открытым исходным кодом Locust для проведения нагрузочного тестирования приложений.

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

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

Первый запуск

Для установки Locust проще всего воспользоваться pip. Выполним:

pip3 install locust

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

from locust import HttpUser, task
            
class User(HttpUser):
    @task
    def mainPage(self):
        self.client.get("/")

Как видно, здесь мы просто будем делать GET запрос. Если вы хотите использовать другое имя для файла, вам нужно будет добавить параметр -f и имя файла при выполнении. Чтобы запустить этот тест, нам нужно будет выполнить команду locust в каталоге скрипта из командной строки, которая запустит веб-интерфейс пользователя на порту 8089:

$ locust

В случае успешного запуска нам становится доступным веб интерфейс на порту 8089, с помощью которого мы можем запускать тесты. При необходимости вы можете изменить порт с помощью команды locust --web-port [порт].

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

В качестве подопытного я развернул контейнер с Nginx на порту 80. Желающие развернуть подобный “стенд” могут это сделать с помощью команды:

docker run --name nginx -d –rm -p 80:80 nginx

Теперь можно указать в качестве параметров Locust 1000 пользователей, 10 секунд и http://адрес_веб_ресурса. Нажимаем Start и наслаждаемся результатами в разных вкладках.

Конечно, когда Failures =0 это как-то не совсем правильно, но у нас всего лишь дефолтная страница Nginx. Итак, все работает, но хотелось бы как-то усложнить тесты.

Для начала запустим тесты с помощью командной строки.

locust --headless -u 1000 -r 10 -H http://адрес

Первый параметр - это количество пользователей, второй это временной интервал и затем сам адрес.

Полная автоматизация

Усложним наше тестирование. Теперь в качестве цели у нас будет использоваться специальный ресурс https://www.demoblaze.com. Это как-бы интернет магазин электроники, разработанный специально для задач тестировщиков.

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

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

Для demoblaze.com мы определим четыре функции, соответствующие нашим четырем шагам: login, clickProduct, addToCart, viewCart и собственно mainPage для открытия основной страницы.

Создадим новый locustfile.py и перезапустим locust.

from locust import HttpUser, SequentialTaskSet, task, between
            
class User(HttpUser):    
    @task
    class SequenceOfTasks(SequentialTaskSet):
        wait_time = between(1, 5)
        @task
        def mainPage(self):
            self.client.get("/")
            self.client.get("https://api.demoblaze.com/entries")
        @task
        def login(self):
            self.client.options("https://api.demoblaze.com/login")
            self.client.post("https://api.demoblaze.com/login",json={"username":"aaaa","password":"YWFhYQ=="})
            self.client.options("https://api.demoblaze.com/check")
            self.client.get("https://api.demoblaze.com/entries")
            self.client.post("https://api.demoblaze.com/check",json={"token":"YWFhYTE2MzA5NDU="})            
        @task
        def clickProduct(self):
            self.client.get("/prod.html?idp_=1")
            self.client.options("https://api.demoblaze.com/check")
            self.client.options("https://api.demoblaze.com/view")
            self.client.post("https://api.demoblaze.com/check",json={"token":"YWFhYTE2MzA5NDU="})
            self.client.post("https://api.demoblaze.com/view",json={"id":"1"})
        @task
        def addToCart(self):
            self.client.options("https://api.demoblaze.com/addtocart")
            self.client.post("https://api.demoblaze.com/addtocart",json={"id":"fb3d5d23-f88c-80d9-a8de-32f1b6034bfd","cookie":"YWFhYTE2MzA5NDU=","prod_id":1,"flag":'true'})
        @task 
        def viewCart(self):
            self.client.get("/cart.html")
            self.client.options("https://api.demoblaze.com/check")
            self.client.options("https://api.demoblaze.com/viewcart")
            self.client.post("https://api.demoblaze.com/check",json={"token":"YWFhYTE2MzA5NDU="})
            self.client.post("https://api.demoblaze.com/viewcart",json={"cookie":"YWFhYTE2MzA5NDU=","flag":'true'})
            self.client.options("https://api.demoblaze.com/view")
            self.client.post("https://api.demoblaze.com/check",json={"token":"YWFhYTE2MzA5NDU="})
            self.client.post("https://api.demoblaze.com/view",json={"id":"1"})

Как вы можете видеть, все методы включены в класс с использованием последовательного набора задач, поэтому они могут выполняться последовательно в том же порядке, в котором они были объявлены. Кроме того, используя wait_time, мы можем добавить паузу между задачами. В данном случае это случайная пауза от 1 до 5 секунд.

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

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

Разные токены

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

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

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

Токен, получаемый в ответе будет иметь например, следующий вид:

"Auth_token: YWFhYTE2MzA1ODg="

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

"Auth_token: (.+?)"

Затем необходимо повторно импортировать модуль и использовать метод match для сохранения извлеченного значения в переменной.

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

  response = self.client.post("https://api.demoblaze.com/login",json={"username":"aaaa","password":"YWFhYQ=="})  

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

global token 
  token = re.match("\"Auth_token: (.+?)\"",response.text)[1]

И мы можем использовать эту переменную в следующих запросах

self.client.post("https://api.demoblaze.com/check",json={"token":token}

Теперь функция login у нас будет иметь следующий вид:

 @task
        def login(self):
            self.client.options("https://api.demoblaze.com/login")
            response = self.client.post("https://api.demoblaze.com/login",json={"username":"aaaa","password":"YWFhYQ=="})
            global token 
            token = re.match("\"Auth_token: (.+?)\"",response.text)[1]
            self.client.options("https://api.demoblaze.com/check")
            self.client.get("https://api.demoblaze.com/entries")
            self.client.post("https://api.demoblaze.com/check",json={"token":token})

А во всех остальных функциях можно вместо захардкоженного токена указывать переменную token. Вот пример для clickProduct:    

        @task
        def clickProduct(self):
            self.client.get("/prod.html?idp_=1")
            self.client.options("https://api.demoblaze.com/check")
            self.client.options("https://api.demoblaze.com/view")
            self.client.post("https://api.demoblaze.com/check",json={"token":token})
            self.client.post("https://api.demoblaze.com/view",json={"id":"1"})

Обрабатываем ответы

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

 Так response.failure(“Error message”) может быть использована для того, чтобы пометить запрос как завершившийся ошибкой. Чтобы утверждение работало должным образом, эту функцию следует использовать внутри if и добавить аргумент catch_response для проверки ответа, как показано ниже.

 @task 
        def viewCart(self):
            self.client.get("/cart.html")
            self.client.options("https://api.demoblaze.com/check")
            self.client.options("https://api.demoblaze.com/viewcart")
            self.client.post("https://api.demoblaze.com/check",json={"token":token})
            with self.client.post("https://api.demoblaze.com/viewcart",catch_response=True,json={"cookie":token,"flag":'true'}) as response:
                if '"prod_id":1' not in response.text:
                    response.failure("Assert failure, response does not contain expected prod_id")
            self.client.options("https://api.demoblaze.com/view")
            self.client.post("https://api.demoblaze.com/check",json={"token":token})
            self.client.post("https://api.demoblaze.com/view",json={"id":"1"})

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

…

                if '"prod_id":123' not in response.text:

…

В результате получаем стабильный набор провальных запросов.

Заключение

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

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

Комментарии (5)


  1. vit1251
    13.10.2024 16:49

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


    1. gigimon
      13.10.2024 16:49

      locust с fasthttpclient для нагрузки http сервисов дает хорошую производительность, но запускать его надо как master-slave по количеству ядер, а еще можно на нескольких серверах


      1. vit1251
        13.10.2024 16:49

        Хорошую, отличную или совсем великолепную? Можно по подробнее, что это качественная оценка означет. Не станет ли проблемой собственная производительность Python в какой-то момент? А если и станет, то какой инструмент внутреннего мониторинга производительности сигнализирует об этом? Понятное дело, что можно масштабировать, но вопрос когда это стоит начинать делать, а то может сразу начинать с использования другиого инструмента и не тратить время. Вот именно об этой неопределенности я и говорю, что это просто такая игрушка попробовать пошевелить систему, но реального понимания вы не достигните, так как не узнаете стоимость самого движка. Стоимость движка вы узнаете только в том случае если начнете еще и профилировать свою систему (не знаю поддерживает ли Python какие-то механизмы Runtime профилирования).


        1. gigimon
          13.10.2024 16:49

          Не самую большую в классе (k6 делает это лучше), но у локуста ее можно наращивать добавлением серверов. По поводу внутреннего торможения из-за питона, это достаточно легко отслеживается по ресурсам сервера и загрузке процессора после парочки тестовых запусков, внутренних механизмов понять это, к сожалению нет. Мы в самом большом сетапе нашем, запускали локуст на 9 серверах по 16 ядер в 9х16 потоков с выделением мастера на отдельный маленький сервер, rps не скажу, т.к. был не совсем http траффик.

          Locust удобный инструмент для проведения быстрого нагрузочного тестирования с получением данных прямо сейчас и регулировкой нагрузки кнопочками в интерфейсе.


          1. vit1251
            13.10.2024 16:49

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

            Выходит механизма для регулирования исходящей нагрузки у данного инструмента нет? А если есть, то как вы можете говорить, что 80% загрузка CPU это не 20% простоя на этом регулировании, а остальное время это перегрузка CPU в циклах Python.

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

            rps не скажу, т.к. был не совсем http траффик.

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

            Locust удобный инструмент для проведения быстрого нагрузочного тестирования

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