Нагрузочное Тестирование С Помощью Locus. Часть 2

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



Обработка ответов сервера

Иногда при нагрузочном тестировании недостаточно просто получить от сервера HTTP 200 OK. Бывает, что все равно нужно проверить содержимое ответа, чтобы убедиться, что под нагрузкой сервер выдает правильные данные или проводит точные расчеты.

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

Рассмотрим следующий пример:

  
  
  
  
  
   

from locust import HttpLocust, TaskSet, task import random as rnd class UserBehavior(TaskSet): @task(1) def check_albums(self): photo_id = rnd.randint(1, 5000) with self.client.get(f'/photos/{photo_id}', catch_response=True, name='/photos/[id]') as response: if response.status_code == 200: album_id = response.json().

get('albumId') if album_id % 10 != 0: response.success() else: response.failure(f'album id cannot be {album_id}') else: response.failure(f'status code is {response.status_code}') class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 max_wait = 2000

Он содержит всего один запрос, который создаст нагрузку по следующему сценарию:
С сервера запрашиваем объекты фотографий со случайными идентификаторами в диапазоне от 1 до 5000 и проверяем id альбома в этом объекте, предполагая, что он не может быть кратен 10.
Здесь можно сразу дать несколько уточнений:
  • ужасающий дизайн с request() в качестве ответа: можно успешно заменить на ответ = запрос() и спокойно работать с объектом ответа
  • URL-адрес формируется с использованием синтаксиса строкового формата, добавленного в Python 3.6, если я не ошибаюсь — f'/photos/{photo_id}' .

    Этот дизайн не будет работать в предыдущих версиях!

  • новый аргумент, который мы раньше не использовали, catch_response=Истина , указывает Locust, что мы сами будем определять успех ответа сервера.

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

  • Еще один аргумент name='/photos/[id]' .

    Нужно группировать запросы в статистике.

    Имя может быть любым текстом; нет необходимости повторять URL. Без него каждый запрос с уникальным адресом или параметрами будет записываться отдельно.

    Вот как это работает:



Нагрузочное тестирование с помощью locus. Часть 2

Используя тот же аргумент, можно проделать еще один трюк — иногда бывает, что один сервис с разными параметрами (например, разным содержанием POST-запросов) выполняет разную логику.

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

Дальше делаем проверки.

У меня их 2. Сначала мы проверяем, что сервер вернул нам ответ if. ответ.status_code == 200 : Если да, то проверяем, кратен ли id альбома 10. Если нет, то отмечаем этот ответ как успешный ответ.успех() В остальных случаях указываем, почему ответ не был получен ответ.failure('текст ошибки') .

Этот текст будет отображаться на странице «Ошибки» во время выполнения теста.



Нагрузочное тестирование с помощью locus. Часть 2

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

Ведь в случае таймаута, ошибки соединения и других непредвиденных происшествий Locust сам обработает ошибки и все равно вернет ответ, хотя и укажет код статуса ответа, равный 0. Если код по-прежнему генерирует исключение, оно будет записано на вкладке «Исключения» во время выполнения, чтобы мы могли его обработать.

Самая типичная ситуация — json-ответ не вернул искомое нами значение, но мы уже выполняем над ним следующие операции.

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

Но вы можете так же легко работать с HTML, XML, FormData, прикрепленными файлами и другими данными, используемыми протоколами на основе HTTP.

Работа со сложными сценариями.

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

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

  1. Открылся основной магазин.

  2. Я искал продукт
  3. Открытая информация о продукте
  4. Добавлен товар в корзину
  5. Оплаченный
Из примера можно предположить, что вызывать сервисы в случайном порядке не получится, только последовательно.

Более того, товары, корзины и формы оплаты могут иметь уникальные идентификаторы для каждого пользователя.

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

Давайте адаптируем пример к нашему тестовому серверу:

  1. Пользователь пишет новое сообщение
  2. Пользователь пишет комментарий к новому сообщению
  3. Пользователь читает комментарий


from locust import HttpLocust, TaskSet, task class FlowException(Exception): pass class UserBehavior(TaskSet): @task(1) def check_flow(self): # step 1 new_post = {'userId': 1, 'title': 'my shiny new post', 'body': 'hello everybody'} post_response = self.client.post('/posts', json=new_post) if post_response.status_code != 201: raise FlowException('post not created') post_id = post_response.json().

get('id') # step 2 new_comment = { "postId": post_id, "name": "my comment", "email": "[email protected]", "body": "Author is cool. Some text. Hello world!" } comment_response = self.client.post('/comments', json=new_comment) if comment_response.status_code != 201: raise FlowException('comment not created') comment_id = comment_response.json().

get('id') # step 3 self.client.get(f'/comments/{comment_id}', name='/comments/[id]') if comment_response.status_code != 200: raise FlowException('comment not read') class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 max_wait = 2000

В этом примере я добавил новый класс Исключение потока .

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

По этой же причине я не использую конструкцию попробуй.

кроме .



Делаем нагрузку реалистичной

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

Разумно, давайте сделаем пример более реалистичным.

И есть как минимум 2 подхода:

  1. Можно «жестко закодировать» список постов, которые читают пользователи, и упростить тестовый код, если это возможно и функциональность бэкенда не зависит от конкретных постов.

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


from locust import HttpLocust, TaskSet, task import random as r class UserBehavior(TaskSet): created_posts = [] @task(1) def create_post(self): new_post = {'userId': 1, 'title': 'my shiny new post', 'body': 'hello everybody'} post_response = self.client.post('/posts', json=new_post) if post_response.status_code != 201: return post_id = post_response.json().

get('id') self.created_posts.append(post_id) @task(10) def read_post(self): if len(self.created_posts) == 0: return post_id = r.choice(self.created_posts) self.client.get(f'/posts/{post_id}', name='read post') class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 max_wait = 2000

В классе Пользовательское поведение Я создал список созданные_посты .

Обратите особое внимание — это объект и он не был создан в конструкторе класса.

__в этом__(), поэтому, в отличие от клиентской сессии, этот список является общим для всех пользователей.

Первая задача создает публикацию и записывает ее идентификатор в список.

Второй читает один случайно выбранный пост из списка в 10 раз чаще.

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

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

class UserBehavior(TaskSet): def __init__(self, parent): super(UserBehavior, self).

__init__(parent) self.created_posts = list()



Еще несколько возможностей

Для последовательного запуска задач официальная документация также предлагает использовать аннотацию задачи @seq_task(1), указав в аргументе порядковый номер задачи.



class MyTaskSequence(TaskSequence): @seq_task(1) def first_task(self): pass @seq_task(2) def second_task(self): pass @seq_task(3) @task(10) def third_task(self): pass

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

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

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



from locust import HttpLocust, TaskSet, task class Todo(TaskSet): @task(3) def index(self): self.client.get("/todos") @task(1) def stop(self): self.interrupt() class UserBehavior(TaskSet): tasks = {Todo: 1} @task(3) def index(self): self.client.get("/") @task(2) def posts(self): self.client.get("/posts") class WebsiteUser(HttpLocust): task_set = UserBehavior min_wait = 1000 max_wait = 2000

В приведенном выше примере вероятность запуска сценария составляет 1 из 6. Делать , и будет выполняться до тех пор, пока с вероятностью 1 из 4 не вернется к сценарию Пользовательское поведение .

Здесь очень важен звонок.

само.

прерывание() — без него тестирование застрянет на подзадаче.

Спасибо за чтение.

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

Теги: #python #тестирование веб-сервисов #тесты производительности #locust

Вместе с данным постом часто просматривают: