Как правильно писать API авто тесты на Python
Вступление
Эта статья как продолжение статьи Как правильно писать UI авто тесты на Python. Если мы говорим про UI автотесты, то тут хотя бы есть паттерны Page Object, Pagefactory; для API автотестов таких паттернов нет. Да, существуют общие паттерны, по типу Decorator, SIngletone, Facade, Abstract Factory, но это не то, что поможет протестировать бизнес логику на уровне API тестируемой системы.
Когда мы пишем API автотесты, то нам хотелось бы, чтобы они отвечали требованиям:
- Проверки должны быть полными, то есть мы должны проверить статус код ответа, данные в теле ответа, провалидировать JSON схему;
- Автотесты должны быть документированными и поддерживаемыми. Чтобы автотесты мог читать и писать не только QA Automation, но и разработчик;
- Хотелось бы, чтобы JSON схема и тестовые данные генерировались автоматически на основе документации;
- Отчет должен быть читабельным, содержав в себе информацию о ссылках, заголовках, параметрах, с возможностью прикреплять какие-то логи.
Для меня требования выше являются базой, ваши же требования могут быть другие в зависимости от продукта.
Также очень важно отметить, что если при написании автотестов вы выберете неправильный подход, то проблемы появляются не сразу, а примерно через 100-150 написанных тестов. Тогда фиксы автотестов превратятся в ад, добавление новых автотестов будет все сложнее и сложнее, а читать такие автотесты никто кроме вас не сможет, что плохо. В практике встречаются случаи, когда компания просит переписать их автотесты и очень часто мотивом является: “Наш QA Automation ушел, поэтому теперь мы не можем даже запустить автотесты и непонятно, что в них происходит”. Это означает, что человек, написавший автотесты, писал их костыльно, как бы повышая свою ценность (в плохом смысле, что никто, кроме него, не сможет понять автотесты в будущем после его ухода или банального ухода на больничный), как сотрудника, что очень плохо для компании. В итоге время потрачено, деньги потрачены.
Еще один распространенный кейс - это когда новый QA Automation приходит на проект и сразу же хочет все переписать. Окай, переписывает, суть не меняется, автоматизация также страдает. По "правильному" мнению человека, который все переписал, виноват продукт, разработчики, но не он сам. Компания в данном случае выступает тренажером/плейграундом для неопытного QA Automation. В итоге время потрачено, деньги потрачены.
Requirements
Для примера написания API автотестов мы будем использовать:
- pytest - pip install pytest;
- httpx - pip install httpx, - для работы с HTTP протоколом;
- allure - pip install allure-pytest, - необязательная зависимость. Вы можете использовать любой другой репортер;
- jsonschema - pip install jsonschema, - для валидации JSON схемы;
- pydantic, python-dotenv - pip install pydantic python-dotenv, - для генерации тестовых данных, для управления настройками, для автогенерации JSON схемы;
Библиотека pydantic служит для валидации, аннотации, парсинга данных в python. Она нам нужна для автогенерации JSON схемы, для описания моделей данных, для генерации тестовых данных. У этой библиотеки есть много плюсов по сравнению с обычными dataclass-сами в python. Если приводить пример из жизни, то pydantic - это как ехать на автомобиле, а dataclass'ы - это идти пешком.
Тесты будем писать на публичный API https://sampleapis.com/api-list/futurama. Данный API всего лишь пример. На реальных проектах API может быть гораздо сложнее, но суть написания автотестов остается та же.
Settings
Опишем настройки проекта. Для этого будем использовать класс BaseSettings из pydantic_settings, потому что он максимально удобный, умеет читать настройки из .env файла, умеет читать настройки из переменных окружения, умеет читать настройки из .txt файла, умеет управлять ссылками на редис или базу данных и много чего еще, можно почитать тут https://docs.pydantic.dev/latest/concepts/pydantic_settings/. Это очень удобно для использования на CI/CD, или когда у вас есть много настроек, которые разбросаны по всему проекту + с BaseSettings все настройки можно собрать в один объект.
settings.py
Мы будем читать настройки из .env файла.
.env
Обратите внимание на то, как записаны переменные окружения TEST_USER.EMAIL и TEST_USER.PASSWORD. Это сделано специально, чтобы "упаковать" значения во вложенную модель TestUser. В данном случае, в качестве разделителя используется точка, но это можно настроить с помощью параметра env_nested_delimiter='.'
Models
Теперь опишем модели, используя pydantic
Модель для аутентификации:
Внутри метода validate_root мы проверяем, был ли передан токен или пользователь при инициализации объекта Authentication. Если не было передано ни того, ни другого, то мы выбрасываем ошибку
Напишем модель для объекта question из API https://sampleapis.com/api-list/futurama. Сам объект выглядит примерно так:
Обратите внимание на аргумент alias в функции Field. Он служит для того, чтобы мы могли работать со snake_case в python и с любым другим форматом извне. Например, в python нам бы не хотелось писать название атрибута таким образом - possibleAnswers, т.к. это нарушает PEP8, поэтому мы используем alias. Pydantic сам разберется, как обработать JSON объект и разобрать его по нужным атрибутам в модели. Так же в функции Field есть очень много крутых фич по типу: max_length, min_length, gt, ge, lt, le и можно писать регулярки. Есть куча полезных настроек для ваших моделей и есть возможность использовать встроенные типы или писать свои. Короче, пользуйтесь.
Данные функции: random_list_of_strings, random_number, random_string используются, чтобы сгенерировать какие-то рандомные данные. Мы не будем усложнять и напишем эти функции, используя стандартные средства python, в своих же проектах вы можете использовать faker.
Готово, мы описали нужные нам модели. С помощью них можно будет генерировать тестовые данные:
JSON схема генерируется автоматически на основе модели. В практике встречал людей, которые писали JSON схему руками, при этом считали это единственным верным подходом, но не нужно так. Ведь если объект состоит из 4-х полей, как в нашем случае, то еще можно написать JSON схему руками, а что если объект состоит их 30-ти полей? Тут уже могут быть сложности и куча потраченного времени. Поэтому мы полностью скидываем эту задачу на pydantic:
API Client, HTTP Client
Теперь опишем базовый HTTP клиент, который будет использоваться для выполнения HTTP запросов, а также API клиент, который будет применяться для создания классов, с помощью, которых будем взаимодействовать с API тестируемой системы:
utils/clients/http/client.py
Мы создали свой класс HTTPClient, который унаследовали от httpx.Client и переопределили необходимые нам методы, добавив к ним allure.step. Теперь при http-запросе через HTTPClient в отчете у нас будут отображаться те запросы, которые мы выполняли. Мы специально использовали allure.step, как декоратор, чтобы в отчет также попали параметры, которые мы передаем внутрь функции метода. Позже посмотрим, как это все будет выглядеть в отчете. Внутрь HTTPClient мы также можем добавить запись логов или логирование в консоль, но в данном примере обойдемся только allure.step, на своем проекте вы можете добавить логирование.
Класс APIClient является базовым для взаимодействия с API системы. В нашем случае мы создаем классы AuthenticationClient и QuestionsClient, которые наследуются от APIClient. Важно подчеркнуть, что класс APIClient предназначен исключительно для взаимодействия с API и не имеет информации о среде, к которой он обращается, а также не знает о токенах и заголовках запросов. Все эти настройки определяются на уровне класса HTTPClient
Напишем билдер, который будет инициализировать класс HTTPClient:
Мы создали функцию get_http_client, которая будет конструировать и возвращать объект HTTPClient. Эта функция будет добавлять базовые атрибуты, заголовки, base_url от которого будем строить ссылки на запросы к API. В этом API https://sampleapis.com/api-list/futurama нет аутентификации, я указал заголовок для аутентификации по API Key ради примера. Скорее всего на вашем проекте у вас будет другой заголовок для аутентификации. AuthenticationClient реализуем ниже
API clients
Теперь опишем методы для взаимодействия с API.
Для примера опишем клиент, который будет работать с аутентификацией. Для https://sampleapis.com/api-list/futurama аутентификация не требуется, но в своем проекте вы можете указать ваши методы для получения токена.
Теперь опишем клиент для работы с API questions:
С помощью клиента QuestionsClient сможем выполнять простые CRUD запросы к API.
Utils
Добавим необходимые утилитки, которые помогут сделать тесты лучше:
Лучше хранить роутинги в enum, чтобы не дублировать код и наглядно видеть, какие роутинги используются:
Фикстура class_questions_client используется для инициализации клиента QuestionsClient. Скоуп специально выбран на класс, потому что не имеет смысла инициализировать данный клиент на каждый тест, возможно из-за специфики тестирования вашей системы, скоуп может быть другой
Для некоторых тестов, например, на удаление или изменение, нам понадобится фикстура function_question, которая будет создавать question. После создания мы будем возвращать объект DefaultQuestion и когда тест завершится, то удалим его delete_question_api(question.id).
Лайфхак. В названиях фикстур используется приставка class_<имя фикстуры> и function_<имя фикстуры>. Это не просто так – приставка соответствует скоупу действия фикстуры, который устанавливается с помощью параметра scope в pytest.fixture(scope="class"). Таким образом, мы включаем информацию о скоупе фикстуры прямо в её название. Это может быть полезно, когда вам нужны одни и те же фикстуры с разными скоупами в разных частях вашего кода.
Например, в некоторых случаях нам может потребоваться создавать нового пользователя для каждого теста, а в других – иметь одного пользователя для всего тестового класса. Используя такое разделение в названиях фикстур, мы делаем код более читабельным и избегаем неявных обозначений скоупов, которые могут ухудшить понимание кода
conftest.py
Не забудем включить наши фикстуры в pytest_plugins. Хотя в принципе вы можете создавать фикстуры непосредственно рядом с вашими тестами в файлах conftest, из моего опыта могу сказать, что это не долгоиграющая история. В реальных проектах бизнес-логика может быть гораздо сложнее, и фикстуры могут иметь иерархию или наследоваться друг от друга.
Например, для добавления пользователя в группу может потребоваться создать группу, затем создать пользователя и только после этого добавить пользователя в группу. Это означает, что у вас может возникнуть сложная иерархия фикстур и их наследование. При использовании отдельных conftest файлов вам могут понадобиться костыли для импортирования фикстур напрямую в эти файлы или сложная организация структуры тестов.
Чтобы избежать таких проблем и упростить жизнь себе и другим автоматизаторам, которые будут писать тесты вместе с вами или после вас, рекомендую использовать плагины pytest. Это позволяет более гибко и эффективно использовать фикстуры и организовывать их наследование
Функция validate_schema будет использоваться для валидации схемы. Можно было бы использовать validate из jsonschema, но тогда мы потеряем allure.step.
Для проверок вы можете использовать обычный assert в python, либо же одну из библиотек: assertpy, pytest-assertions. Но мы будем использовать кастомную реализацию expect, которая будет включать в себя allure.step или другой удобный для вас репортер. Стоит отметить, что в библиотеке pytest-assertions также есть встроенные allure.step.
Реализацию expect вы можете посмотреть тут https://github.com/Nikita-Filonov/sample_api_testing/tree/main/utils/assertions/base. По этой ссылке код достаточно объемный, поэтому я не буду разбирать его в статье.
Также добавим функцию, которая будет проверять корректность объекта question, который вернуло на API.
Эта функция служит для того, чтобы нам не приходилось в каждом тесте писать заново все проверки для объекта question и достаточно будет использовать функцию assert_question. Если у вас объект состоит из множества ключей (например, 20), то рекомендую писать такие обертки, чтобы использовать их повторно в будущем.
Также обратите внимание на QuestionDict - это не модель, это TypedDict и он служит для аннотации dict в python. Лучше стараться писать более конкретные типы вместо абстрактного dict, учитывая, что аннотации в python - это просто документация и не более. Ибо в будущем абстрактные аннотации будут только затруднять понимание кода. Даже если вы пишете просто тип int, то лучше писать что-то конкретное по типу MyScoreInt = int.
Testing
Мы подготовили всю базу для написания тестов. Осталось только написать сами тесты:
Тут 5-ть тестов на стандартные CRUD операции для questions API https://api.sampleapis.com/futurama/questions.
Возвращаясь к нашим требованиям:
- Проверяем статус код ответа, тело ответа, JSON схему;
- При создании объекта внутри метода create_question у нас происходит автоматическая валидация на основе модели pydantic DefaultQuestion(**response.json()). Это автоматически избавляет нас от необходимости писать проверки для ответа API;
- Автотесты документированы и легко читаются. Теперь другой QA Automation или разработчик, когда посмотрит на наши тесты, сможет увидеть аннотацию в виде моделей. Посмотрев на модели, он сможет легко разобраться с какими именно объектами мы работаем. В pydantic имеется возможность добавлять description к функции Field, поэтому при желании вы сможете описать каждое поле вашей модели;
- JSON схема генерируется автоматически, рандомные тестовые данные тоже генерируются автоматически на основе модели. При большой мотивации вы можете взять ваш Swagger и вытащить из него JSON схему с помощью https://github.com/instrumenta/openapi2jsonschema. Далее y pydantic есть убойная фича https://docs.pydantic.dev/datamodel_code_generator/ и на основе JSON схемы pydantic сам сделает нужные модели. Этот процесс можно сделать автоматическим.
Report
Запустим тесты и посмотрим на отчет:
Теперь запустим отчет:
Либо можете собрать отчет и в папке allure-reports открыть файл index.html:
Получаем прекрасный отчет, в котором отображается вся нужная нам информация. Вы можете модифицировать шаги под свои требования или добавить логирование для каждого HTTP запроса.
Заключение
Весь исходный код проекта расположен на моем github.
Всегда старайтесь писать автотесты так, чтобы после вас их смог прочитать любой другой QA Automation или разработчик; желательно не только прочитать и понять, но и легко починить, если потребуется. Не повышайте свою ценность для компании через "магический код" понятный только вам.
UPD. Статья была обновлена, поскольку код из неё морально устарел. Кроме того, версия pydnatic была обновлена до версии 2.4, и в данной версии были устранены некоторые баги, которые ранее требовали использования костылей. Если вам интересно посмотреть на старую версию автотестов, вы можете найти её здесь