Автоматизация тестирования 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

Спасибо, что прочел до конца, надеюсь Вам поможет эта статья)


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