Автоматизация тестирования API посредством Python
Доброго времени суток! В этой статье я собираюсь продолжить рассказ о своем небольшом опыте автоматизации. В прошлой статье я показал, как это сделать с помощью Postman — сегодня покажу, как это реализовать, используя язык программирования Python, фреймворк Pytest, библиотеку Requests.
Для начала представлю дерево проекта.
Дерево проекта
Далее о вспомогательных файлах: helpers.py, configKey.py.
В helpers.py генерируются три токена проекта: основной и два интегрирующих сервиса, а также используется метод валидации схем ответов.
import pytest
from jsonschema import validate as validate_json, ValidationError
import requests as r
from configKey import *
import time
today = time.strftime("%Y-%m-%d", time.localtime())
def validate_schema(response, schema):
try:
validate_json(response, schema)
except ValidationError as e:
print(e)
return False
return True
def update_headers():
"""Первичная проверка на доступ сервера"""
try:
response = r.post(**_url, json=**_person_data, verify=False)
**_headers['Authorization'] = response.text
response = r.get(url=***_url, headers=***_headers, verify=False)
***_headers['Authorization'] = 'Bearer ' + response.json()['****']
response = r.get(url=****_url, headers=**_headers, verify=False)
****_headers['Authorization'] = 'Bearer ' + response.json()['*****']
except:
return False
return True
Принцип генерации токена следующий — отправляется запрос на сервер, из ответа извлекается непосредственно токен, он вводится в отдельную и переменную и переиспользуется в тестах при импорте helpers.py. Самое главное — два импорта: Pytest, requests — первый для запуска тестов, второй для использования библиотеки, которая будет отправлять запросы.
В configKey основные базовые урлы и схемы хедеров.
Непосредственно к тестам. Сервис ЕМИАС. Необходимо выполнить следующие импорты.
import pytest
import requests as r
from configKey import *
from schemas.schemas_valid import *
from helpers import *
Далее вводим класс, в котором будут переменные, методы и тесты. Самое главное — название класса должна начинаться на «Test».
class Test***:
"""Классовые переменные с динамическими данными"""
visit_id_home = '' """Айди вызова врача на дом"""
visit_id = '' """Айди записи к врачу"""
available_time = '' """Талон к врачу"""
@classmethod
def setup_class(cls):
"""Принудительное обновление токенов"""
if not update_headers():
pytest.exit('Ошибка получения токенов', 3)
@classmethod
def teardown_class(cls):
"""Удаление данных"""
update_headers()
if Test***.visit_id_home:
r.delete(f'{link}/*******/{Test***.visit_id_home}', headers=***_headers)
if Test***.visit_id:
r.delete(f'{link}/*****/{Test***.visit_id}', headers=***_headers)
На 7-ой и 13-ой строках я ввожу классовые методы: setup_class (cls), teardown_class (cls) — для того, чтобы при запуске любого теста в этом классе данные методы обязательно вызывались. setup_class (cls) — обновление токенов, которые генерируются в helpers.py перед запуском, teardown_class (cls) — удаление данных после запуска. Ps: в правилах хорошего тона очищать за собой пространство от тестовых данных во избежание дубликатов или отсутствия возможности записаться, так как активная запись может быть только одна — повалятся ошибки 400 «Bad request».
Флоу пользователя я описывал в прошлой статье, можете ознакомиться там) — это статья, как дополнение к предыдущей.
Непосредственно к тестам. Название тестов должны начинаться с «test», чтобы pytest мог их идентифицировать. Не забываем соблюдать табуляцию — наши тесты находятся внутри класса, поэтому классовые методы и тесты на один Tab вправо.
Перед тестом я использую два метода для парсинга тел ответов.
@classmethod
def parse_doctor_data(cls, response):
items = response.json()['items'][0]
for doctor in items['doctors']:
for sched in doctor['schedule']:
if sched['count_tickets'] > 0:
print(sched['date'])
return {
'available_date' : sched['date'][0:10],
'doctor_id' : doctor['id'],
'lpu_code' : doctor['lpu_code'],
}
return {}
@staticmethod
def parse_ticket_time_data(response):
schedule = response.json()['schedule']
for sched in schedule:
if not sched['busy']:
return sched['time']
return ''
На 3-ей строке я ввожу переменную items, где меня интересует первый открытый день для записи. На 4-ой строке я запускаю цикл, который проходит по каждому доступному врачу. Далее — на 5-ой строке я запускаю вложенный цикл, в котором хочу получить все расписания свободных дней для записи. На 6-ой строке я уточняю, есть ли наличие свободных талонов, если все условия выполняются, то я прошу записать данные в переменные глобального окружения. Вложенный цикл используется потому, что массив тела ответа состоит из нескольких слоев.
def test_visit_to_doctor(self):
"""Получение свободных талонов к врачу"""
url = f'{link}/*********'
response = r.get(url, headers=***_headers)
assert response.status_code == 200, f'Ожидался статус код 200 ОК, но получен {response.status_code}'
assert validate_schema(response.text, test_visit_to_doctor_1), 'Json schema does not match'
doctor_data = TestEmias.parse_doctor_data(response)
"""Получение талонов к врачу по конкретному дню."""
assert doctor_data['available_date'] and doctor_data['doctor_id'], "Данные о враче не были установлены."
url = f"{link}/*****/{doctor_data['doctor_id']}?*****={doctor_data['available_date']}"
response = r.get(url, headers=***_headers)
assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'
assert validate_schema(response.text, test_visit_to_doctor_2), 'Json schema does not match'
Test***.available_time = Test***.parse_ticket_time_data(response)
"""Запись на прием к врачу."""
assert TestEmias.available_time, "Время талона не было установлено."
url = f'{link}/*****'
payload = {
"doctor_id": doctor_data['doctor_id'],
"email": "***@yandex.ru",
"lpu_code": doctor_data['lpu_code'],
"day": doctor_data['available_date'],
"time": Test***.available_time
}
response = r.post(url=url, headers=***_headers, json=payload)
assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'
assert validate_schema(response.text, test_visit_to_doctor_3), 'Json schema does not match'
Test***.visit_id = response.json()['entry_id']
"""Отмена записи к врачу"""
assert Test***.visit_id, 'Не высталвлен visit_id'
response = r.delete(url=url + f'/{Test***.visit_id}', headers=***_headers)
assert response.status_code == 204, f'Ожидался статус-код ОК, но получен {response.status_code}'
assert validate_schema(response.text, test_visit_to_doctor_4), 'Json schema does not match'
Test***.visit_id = ''
Для того, чтобы отправить запрос в рамках теста нам нужно:
1) Обозначить тест, что и сделано в первой строке;
2) Ввести обязательные параметры запроса: url, headers, body (post, put, patch — запросы).
2.1) URL — указываем адрес, в моем примере я вызываю {link} , потому что это базовый урл, который лежит в helpers.py, а под звездочками — ендпоинты, если будете указывать прямую ссылку, то это будет выглядеть примерно так: url = 'https://habr.com/ru/article/new/'.
2.2) Headers — т.к. у меня хедеры тянутся из helpers.py, я их импортирую, поэтому я их не ввожу.
2.3*) Если у Вас Post, Put или Patch запрос, то добавьте строчку 'payload' или назовите по-своему.
3) Отправка запроса — т.к. я импортировал библиотеку «Response as r» мои запросы выглядят следующим образом (три обязательных аргумента, если запрос Post):
response = r.post(url=url, headers=***_headers, json=payload)
4) Проверка — это может быть проверка на статус-код, на наличие конкретного ключа в ответе, на время ответа или проверка схемы — проверки используются индивидуально, но на статус-код проверяем почти всегда.
assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'
Конструкция assert работает следующим образом (для тех, кто не в курсе): если возвращается 200 статус-код, то все окей, а если возвращается другой статус код, например, 403, то тогда срабатывает assert и возвращает вам код ошибки — исходя из второй части верхней проверки "{response.status_code}'{response.status_code}".
5*) Распечатывать ответ или его часть. Например: print(response.text)
Можно распечатать URL, новую переменную или тело запроса для дебага, но обычно для этого используют pdb (python debug), но можно и прямо в тестах, но локально)
6*) В моем случае мне необходимо записать тело ответа в переменную класса для ее последующего использования.
Test***.visit_id = response.json()['entry_id']
Под звездами — название класса, визит_айди — название новой переменной (не забудьте ее предварительно указать в начале класса) = конкретный ключ из ответа запишется в переменную.
6*) Схемы валидации. Обсудите с командой, нужны ли Вам такие проверки. В отдельном файле «schemas_valid.py». Они выглядят примерно так:
test_doctor_visit_home_schema_3 = {
"code": 200,
"message": "Вызов отменен",
"message_code": "2000"
}
test_banners_schema = {
"required": [
"content",
"pageable",
"totalPages",
"totalElements",
"last",
"number",
"size",
"sort",
"numberOfElements",
"first",
"empty"
]
}
test_sessionId_schema = {
"sessionId": str,
"required": ["sessionId"]
}
7) Запуск. Советую запускать из терминала IDE или системного терминала по следующей команде: pytest -s -v -k "название теста"
Либо запустить целиком файл.
Правой кнопкой мышью тапаем по файлу и копируем относительный путь
Пишем: Pytest + вставляем из буфера обмена относительный путь + »-s -v»
pytest tests/smoke_test/high/test_003.py -s -v
Примерные ответы тестов
8) Убедитесь, что файлы с тестами лежат в папке «test» и названия файлов, классов и тестов начинаются на 'test'. Проверьте, чтобы в 'requirements.txt' были следующие зависимости:
jsonschema==4.22.0
pytest==7.4.4
Requests==2.32.3
Чтобы Вам не мешали лишние библиотеки — удалите файл 'requirements.txt' В ДАННОМ ПРОЕКТЕ и выполните команду: pip3 install pipreqs
Спасибо, что прочел до конца, надеюсь Вам поможет эта статья)