Автоматизация тестирования 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 основные базовые урлы и схемы хедеров.

6c770afae3fee410ece8205e6e594f05.png

Непосредственно к тестам. Сервис ЕМИАС. Необходимо выполнить следующие импорты.

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

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

© Habrahabr.ru