Python. Тестирование API. Структура тестов

от автора

Всем привет, меня зовут Александр, в последние 2 года занимаюсь автоматизированным тестирование. Хочу поделиться своим наработанным опытом по созданию API тестов. Для написания автотестов в компании используем selenium webdriver, behave.

Behave — это фрейморк для программирования через поведение системы в python-стиле. Behave использует тесты, написанные на “естественном”, то есть, английском языке.

Selenium webdriver широко используется и думаю в представлении не нуждается, но всегда можно загуглить.

Структура тестов behave очень проста, если feature файлы в которых описываются сценарии и папка steps в которой находятся шаги для выполнения этих сценариев.

Первое на что можно обратить внимание это на структуру feature файлов (в этих файлах находятся сами тесты, которые состоят из различных шагов):

@api @userportal @userportal_api @maps @userportal_maps @userportal_maps_api @requests Feature: Requests userportal maps    @get @get_maps_drivers_driverId   Scenario Outline: Assert GET request maps/drivers/{driverId}     Given get token     Then Assert GET request maps drivers/<driver_id> - check <check>     Examples:       | driver_id  |    check    |       |100         |status_code  |       |[200, 159]  |structure    |       |random 100  |response     |       |random: 200 |all          |    @get_maps_factories_factoryId @get   Scenario Outline: Assert GET request maps/factories/{factoryId}     Given get token     Then Assert GET request maps factories/<factoryId> - check <check>     Examples:       | factoryId  |    check    |       | random: 5  |status_code  |       | random: 5  |structure    |       |all         |response     |       |all         |all          |

В этом примере будет выполнено 8 тестов, по 4 на каждый из описанных сценариев.

Для сокращения количества выполняемых тестов, если запрос по всем 3м проверкам выполняется без ошибок, то эти проверки можно закомментировать и будет выполняться только полная проверка запроса (check -> all). Проверки разбиваются для того, чтобы можно было увидеть какие тесты данного метода автоматизированы и для быстрого поиска проблемы, если тест упал.

При создании теста в первую очередь ориентируюсь на метод запроса и название самого метода. Считаю, что хорошее название API теста должно состоять 1) Из метода запроса 2) короткий путь (без base url). Я реализую следующие проверки на текущем проекте: 1) Статус код 2) Структура ответа 3) Сам ответ 4) Фильтры. Можно расширить еще такими: 5) Время выполнения 6) Уровень доступа и тд. (можете добавить свои варианты)

Параметры для шагов сценариев передаются в example.

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

import time import requests from BDDCommon.CommonConfigs.urlconfig import URLCONFIG from BDDCommon.configs import current_settings, STANDS   token = None expiration_time = 0   @step('get token') def get_token(context):     context.token_auth = get_auth_token()   def get_auth_token():     global token     global expiration_time      current_time = int(time.time())     # Если нет токена или время жизни токена истекло - получаем новый токен     if (token is None) or (current_time > expiration_time):         company = current_settings['company']         data_login = {             'username': STANDS[current_settings['stand']]['users']['company_users'][company]['main_logist']['login'],             'password': STANDS[current_settings['stand']]['users']['company_users'][company]['main_logist']['pwd']}         url = URLCONFIG['base_url_not_os'] + URLCONFIG['login_token']         token = requests.post(url, data=data_login)         assert token.status_code == 200, f'Error auth {token.status_code} - {token.text}'         token = token.json()         token = {'authorization': 'Bearer ' + token['resp']['accessToken']}         expiration_time = current_time + 1800  # допустимый срок действия токена 30 минут      return token

Сам шаг с проверкой выглядит следующим образом:

@step('Assert GET request maps drivers/{driver_id} - check {check}') def assert_req_maps_drivers_driver_id(context, driver_id, check):     # Функция для проверки GET запроса maps/drivers/{driver_id}     # driver_id может быть числом, "all", "random ЧИСЛО"     # check может быть: "all" "structure" "response", "status_code"     ......     driver_id_list = get_all_drivers(company_id=current_settings['company'])     # Проходимся по каждому водителю в списке     for driver_el in driver_id_list:         # Отправляем запрос         response = get_req_maps_drivers_by_driver_id(driver_el, context.token_auth)         url = f"{URLCONFIG['base_url_not_os']}{URLCONFIG['base_api_url']}" \               f"{URLCONFIG['maps']}{URLCONFIG['drivers']}/{driver_el}"          # Запускаем проверки, в зависимости от check         if check == 'status_code' or check == 'all':             logger.info(f'Check status_code. Method: GET URL: {url}')             assert response.status_code == 200, \                 f'URL: {url} GET request.status_code Actual: {response.status_code} Expected: 200\n' \                 f'Response: {response.text}'          if check == 'structure' or check == 'all':             logger.info(f'Check structure response. Method: GET URL: {url}')             check_structure_maps_drivers_driverId(response.json()['resp'], driver_el)          if check == 'response' or check == 'all':             logger.info(f'Check response. Method: GET URL: {url}')             check_response_maps_drivers_driverId(response.json()['resp'], driver_el)          # Вызов ошибки если передали не поддерживающий check         if check not in ['all', 'response', 'structure', 'status_code']:             raise Exception(f'not valid param check: {check} in function assert_get_req_maps_drivers_driver_id')      logger.info(f'assert request maps/drivers/driverId Check: {check} - Completed\n driverIds: {driver_id_list}')

Проверка структуры ответа выглядит следующим образом:

def check_structure_maps_drivers_driverId(resp):   assert 'vehicleNumber' in resp   assert type(resp['vehicleNumber']) in [str, NoneType]) .........

Проверяем, что все необходимые поля есть в ответе и что их тип соответствует ожидаемому.

Проверка ответа запроса.

def check_response_maps_drivers_driverId(response, driver_id):     driver_obj = Driver_obj(driver_id)      expected_response = {         'id': driver_obj.id,         'vehicleNumber': driver_obj.vehicle_number,         'fullName': driver_obj.name,         ....     }     assert response == expected_response, f'Error response.\nActual response:\n{response}\nExpected: {expected response}'

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

Подводя итоги, хотелось бы подчеркнуть главное:

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

  2. Запрос отдельно — проверки отдельно

  3. Разбиение проверок на разные функции

  4. Название теста должно отражать название запроса

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

its my first page article


ссылка на оригинал статьи https://habr.com/ru/articles/782502/