[Перевод] Автоматизированное тестирование API с использованием Python. Работа с JSON и JsonPath

0dedac8e879ca70499c784e13adc20a5.png

JSON — один из самых распространённых форматов данных, используемых для передачи и получения данных в современных API. Важно глубоко понять его. 

Если вы совсем не знакомы с этим форматом, рекомендуем ознакомиться с ресурсами JSON.org или w3schools, чтобы получить общее представление. 

Здесь я дам краткий обзор: в основном это структура данных вида key: value, содержащая примитивные типы данных, такие как строка, логическое значение, числа, а также массивы. JSON очень похож на словарь в Python. 

На самом деле, если вы уже работали с вложенными словарями, то уже имеете представление о JSON. 

Перед тем как продолжить, давайте разберём несколько ключевых определений, которые часто встречаются при работе с JSON в автоматизации API:  

  • Процесс преобразования объекта Python в JSON называется сериализацией

  • Процесс преобразования JSON в объект Python называется десериализацией

Работа с JSON 

В стандартной библиотеке Python есть встроенная поддержка JSON через модуль JSON. Есть несколько распространённых сценариев его использования:  

Типичный процес тестирования API

Допустим, мы хотим автоматизировать следующий сценарий для нашего people API:

  1. Прочитать JSON из файла.
    (Это может быть полезно, если вы хотите хранить тело запроса в качестве шаблона отдельно, а не указывать его прямо в тестах или файле с тестовыми данными.)

  2. Изменить определённый параметр в запросе.

  3. Преобразовать словарь Python в строку JSON.

  4. Передать JSON-нагрузку в POST-запрос для создания пользователя с использованием people API.

  5. Получить всех пользователей в текущей базе данных с помощью GET API.

  6. Увебдиться, что новый пользователь создан в системе, используя JSONPath вместо ручного разбора данных.

Я уже создал тест для этого сценария. Давайте разберём его разные части:  

tests/data/create_person.json

{
  "fname": "Sample firstname",
  "lname": "Sample lastname"
}}

Во-первых, в каталоге tests/data есть файл create_person.json, который представляет собой пример тела запроса (также часто называемый полезной нагрузкой запроса). 

Это в целом хорошая практика, так как позволяет не указывать тела запросов непосредственно в тестах и делает их более компактными, особенно если у вас большой объём данных.

utils/file_reader.py

import json
from pathlib import Path

BASE_PATH = Path.cwd().joinpath('..', 'tests', 'data')


def read_file(file_name):
    path = get_file_with_json_extension(file_name)

    with path.open(mode='r') as f:
        return json.load(f)


def get_file_with_json_extension(file_name):
    if '.json' in file_name:
        path = BASE_PATH.joinpath(file_name)
    else:
        path = BASE_PATH.joinpath(f'{file_name}.json')
    return path

Далее у нас есть utils/file_reader.py, который предоставляет функцию для приёма имени файла из каталога tests/data, чтения его содержимого и возвращения JSON-строки.

Несколько важных моментов:

with path.open(mode='r') as f:
    return json.load(f)
  • Обратите внимание, что мы используем path.open вместо метода open из Python напрямую. Это позволяет использовать класс Path из модуля pathlib для удобного построения пути к файлу (кроссплатформенное решение) и упрощает работу с ним.

  • У нас также есть метод get_file_with_json_extension, который добавляет расширение .json, если у файла его нет.

  • Мы используем json.load(), чтобы напрямую передать в него файл для чтения и получить объект Python.

Итак, это помогает нам получить объект Python.

tests/people_test.py

Теперь посмотрим, как использовать это в нашем тесте. 

Ниже приведён полный файл теста. Давайте разберём изменения.

@pytest.fixture
def create_data():
    payload = read_file('create_person.json')

    random_no = random.randint(0, 1000)
    last_name = f'Olabini{random_no}'

    payload['lname'] = last_name
    yield payload


def test_person_can_be_added_with_a_json_template(create_data):
    create_person_with_unique_last_name(create_data)

    response = requests.get(BASE_URI)
    peoples = loads(response.text)

    # Получить все фамилии для любого объекта в корневом массиве
    # Здесь $ = корень, [*] представляет любой элемент в массиве
    # Полный синтаксис: https://pypi.org/project/jsonpath-ng/
    jsonpath_expr = parse("$.[*].lname")
    result = [match.value for match in jsonpath_expr.find(peoples)]

    expected_last_name = create_data['lname']
    assert_that(result).contains(expected_last_name)


def create_person_with_unique_last_name(body=None):
    if body is None:
        # Гарантировать, что пользователь с уникальной фамилией создается каждый раз, когда выполняется тест
        # Примечание: json.dumps() используется для преобразования словаря Python в строку JSON
        unique_last_name = f'User {str(uuid4())}'
        payload = dumps({
            'fname': 'New',
            'lname': unique_last_name
        })
    else:
        unique_last_name = body['lname']
        payload = dumps(body)

    # Установка стандартных заголовков для указания, что клиент принимает JSON
    # и будет отправлять JSON в заголовках
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }

    # Мы используем метод requests.post с ключевыми параметрами для повышения читаемости запроса
    response = requests.post(url=BASE_URI, data=payload, headers=headers)
    assert_that(response.status_code, description='Person not created').is_equal_to(requests.codes.no_content)
    return unique_last_name

Используем фикстуру pytest для настройки данных.

@pytest.fixture
def create_data():
    payload = read_file('create_person.json')

    random_no = random.randint(0, 1000)
    last_name = f'Olabini{random_no}'

    payload['lname'] = last_name
    yield payload

В приведённом примере, вместо того чтобы включать весь код настройки прямо в тестовый метод, я использую фикстуры pytest, для передачи данных в тесты. Обратите внимание, что фикстура с именем create_data передаётся в качестве аргумента тестовому методу: def test_person_can_be_added_with_a_json_template(create_data):

Мы получаем словарь в Python как полезную нагрузку, используя read_file('create_person.json'), затем с помощью модуля random генерируем случайное число от 0 до 1000 и добавляем его к префиксу. 

В конечном итоге мы обновляем это значение в теле запроса и передаём его тестовому методу, используя ключевое слово yield.

Мы также модифицируем ранее созданную функцию create_person_with_unique_last_name, чтобы она могла принимать тело запроса при необходимости, задав его значение по умолчанию как None, и использовать это значение для создания тела JSON-запроса с помощью метода json.dumps(). Если тело запроса не передано, сохраняется предыдущая функциональность генерации тела запроса.

Использование JSONPath

Наконец, после создания пользователя, давайте посмотрим, как можно использовать JSONPath для извлечения значений из JSON.

# Получить все фамилии для любого объекта в корневом массиве
# Здесь $ = корень, [*] представляет любой элемент в массиве
# Полный синтаксис: https://pypi.org/project/jsonpath-ng/
jsonpath_expr = parse("$.[*].lname")
result = [match.value for match in jsonpath_expr.find(peoples)]

expected_last_name = create_data['lname']
assert_that(result).contains(expected_last_name)

JSONPath — это отличный способ работы с длинной вложенной JSON-структурой, предоставляющий возможности, похожие на XPath. Чтобы добавить эту библиотеку в наш фреймворк, используйте следующий код:

pipenv install jsonpath-ng

Для полного описания различных вариантов использования этой библиотеки обратитесь к странице PyPI, jsonpath-ng.

Пример

В нашем случае предположим, что мы хотим выполнить то же действие, что и раньше, то есть получить все фамилии пользователей и проверить, присутствует ли ожидаемая фамилия в этом списке. 

Мы можем задать выражение JSONPath, используя метод parse("$.[*].lname").

Это выражение интерпретируется следующим образом:

  • $ — начало от корня,

  • [*] — любой элемент внутри массива,

  • .lname — получение значений ключей с именем lname.

Чтобы выполнить этот JSONPath-запрос, мы вызываем метод find() и передаём в него JSON-ответ, полученный из GET-запроса API. 

Наконец, мы проверяем, что наша ожидаемая фамилия действительно присутствует в этом списке пользователей, и тест завершится с ошибкой, если фамилия не найдена.

Заключение

В этой статье мы рассмотрели:

  • Как сериализовать и десериализовать JSON,

  • Как его изменять,

  • И, наконец, как использовать расширенные возможности парсинга JSON.

Понимание этих концепций будет полезно для создания прочной основы успешного фреймворка для тестирования API.

Всех желающих приглашаем принять участие в открытых уроках, которые пройдут в рамках курса «Python QA Engineer»:

  • 12 ноября: «Pytest hooks в автоматизации тестирования на Python» — разберем, как хуки помогают управлять жизненным циклом тестов, выполнять различные задачи до и после тестов. Записаться

  • 21 ноября: «Тестируем REST API‑сервисы на Python» — разберем библиотеку requests, разработаем HTTP‑клиент для использования в тестах, напишем тесты на REST API с помощью PyTest. Записаться

© Habrahabr.ru