JSON — один из самых распространённых форматов данных, используемых для передачи и получения данных в современных API. Важно глубоко понять его.
Если вы совсем не знакомы с этим форматом, рекомендуем ознакомиться с ресурсами JSON.org или w3schools, чтобы получить общее представление.
Здесь я дам краткий обзор: в основном это структура данных вида key: value, содержащая примитивные типы данных, такие как строка, логическое значение, числа, а также массивы. JSON очень похож на словарь в Python.
На самом деле, если вы уже работали с вложенными словарями, то уже имеете представление о JSON.
Перед тем как продолжить, давайте разберём несколько ключевых определений, которые часто встречаются при работе с JSON в автоматизации API:
-
Процесс преобразования объекта Python в JSON называется сериализацией.
-
Процесс преобразования JSON в объект Python называется десериализацией.
Работа с JSON
В стандартной библиотеке Python есть встроенная поддержка JSON через модуль JSON. Есть несколько распространённых сценариев его использования:
-
Преобразовать словарь Python в формат JSON для передачи в запросе:
-
используйте
json.dump()
, если нужно записать данные в файл -
или
json.dumps()
, если нужно сохранить данные в строку Python.
-
-
Преобразовать JSON в словарь Python:
-
используйте
json.load()
, если хотите читать JSON напрямую из файла, -
или
json.loads()
, если читаете JSON из строки Python
-
Типичный процес тестирования API
Допустим, мы хотим автоматизировать следующий сценарий для нашего people API:
-
Прочитать JSON из файла.
(Это может быть полезно, если вы хотите хранить тело запроса в качестве шаблона отдельно, а не указывать его прямо в тестах или файле с тестовыми данными.) -
Изменить определённый параметр в запросе.
-
Преобразовать словарь Python в строку JSON.
-
Передать JSON-нагрузку в POST-запрос для создания пользователя с использованием people API.
-
Получить всех пользователей в текущей базе данных с помощью GET API.
-
Увебдиться, что новый пользователь создан в системе, используя 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. Записаться
ссылка на оригинал статьи https://habr.com/ru/articles/857726/
Добавить комментарий