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/ - 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}'
Это самый трудоемкий процесс, но и самый интересный. В процессе расширяются методы классов, находятся ошибки в коде, происходит развитие в понимании того, как можно улучшить или ускорить свой код.
Подводя итоги, хотелось бы подчеркнуть главное:
Для более быстрой работы тестов позаботьтесь о том, чтобы не выполнялась авторизация при запуске каждого теста
Запрос отдельно — проверки отдельно
Разбиение проверок на разные функции
Название теста должно отражать название запроса
Вопросы, критику пишите в комментарии, по возможности буду отвечать.
its my first page article