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

0acdd01b0282fd1e91ada34165313bc3

Всем привет, меня зовут Александр, в последние 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/ - 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/ - 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

© Habrahabr.ru