Извлекаем данные с помощью языка запросов JsonPath библиотеки Rest Assured

logo

Традиционно начну с небольшого вступления: речь в статье пойдет о том, как достать нужные данные из JSON, используя библиотеку JsonPath, которая входит в состав Rest Assured – наиболее популярного решения для тестирования API. Есть и другие одноименные библиотеки (JsonPath), которые используют другой синтаксис, это даже специально подчеркнули в доках Rest Assured во избежание путаницы.

JsonPath основан на Groovy Gpath и использует его для построения своих запросов, условно можно сказать, что это xpath для json – язык запросов для получения нужных данных. К сожалению не все из нас владеют Груви (я совсем не знаю), а вот нормальной документации по использованию языка запросов JsonPath в более-менее развернутом виде я не нашел. Вот доки GPath в груви, на котором все работает, вот доки самого класса с парой примеров, и конечно документация Rest-Assured

Итак, мы не рассматриваем, что такое JSON, как пользоваться Rest-Assured, какие методы есть в JsonPath для парсинга, вытягивания Листа или Мапы, начинаем сразу с того, что у нас есть некий json, из которого мы хотим получить различные данные. При этом нам не хочется или нет надобности создавать простые Java-классы и маппить данные из json на них, мы просто хотим вытащить необходимое просто и быстро, без написания лишнего кода. Библиотека JsonPath хоть и поставляется в составе RA, но может быть использована отдельно. Для тренировки или проверки, можете подтянуть зависимость

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>json-path</artifactId>
    <version>3.3.0</version>
</dependency>

Ну а сам код для простоты будет вот таким

String expr = ""; // тут будет запрос
System.out.println(JsonPath.from(json).getString(expr)); //выводим результат

Еще раз подчеркну, что мы не рассматриваем методы типа setRoot, getList, getMap и так далее, сосредоточимся на языке запросов. Вот json с которым мы будем работать:

{
  "class": [
    { "name": "Ivan",
      "surname": "Ivanov",
      "age": "13",
      "marks": [2,3,4,4,5],
      "address": {
        "street": "Lenin",
        "house": 12
      }
    },
    { "name": "Petr",
      "surname": "Petrov",
      "age": "14",
      "marks": [5,5,3,5],
      "address": {
        "street": "Marx",
        "house": 7
      }
    },
    { "name": "Ivan",
      "surname": "Sidorov",
      "age": "14",
      "marks": [3,4,2,2,3],
      "address": {
        "street": "Pobedy",
        "house": 9
      },
      "some.value": 42
    }]
}

Он составлен в учебных целях, для примера некоторых особенностей, обратите внимание, что возраст возвращается строкой («age»: «14»), а не числом, и, кроме того, у третьего ученика в классе есть уникальное поле «some.value»: 42 , которое тоже добавлено специально.

1.Начнем с самого простого – корень, индексы, точки

$  — корень нашего json, просто выведет его целиком (то, что внутри главных скобок{}), как и при использовании пустой строки

Значение любого поля можно получить указав его ключ, но поиск начинается от корня, потому если поле вложенное, то нужно указать путь до него с использованием точки корень.поле1.поле2

class – вернет список из трех объектов-учеников, это единственное поле в корне нашего json, все остальные данные уже вложены в него, поэтому путь к ним нужно будет начинать с указания “class”. Если, например написать “name” то нам вернет null, так как в корне нет такого ключа. Вообще такой подход аналогичен работе с XPath – начинаем от корня и двигаемся к искомой информации вглубь документа.

Работа со списком (массивом) аналогично Java с некоторыми замечаниями – первый индекс привычно 0, выходить за границы списка нельзя, но можно использовать отрицательные индексы и слайсинг как в питоне.

securityservВажно! Все эти операции (индекс, слайсинг) применимы и к строке!

class[0] – вернет нам первый объект-ученик, а именно Иванова Ваню

class[-1] – вернет нам последний элемент списка то есть объект Сидорова Вани, аналог “class[2]

class[-2]  — вернет предпоследний элемент списка (и так далее до выхода за пределы массива), то есть Петрова. Такой же результат даст вызов “class[1]

class[0..1] – вырежет из списка и вернет элементы ОТ нулевого (по индексу) ДО первого включительно, то есть вернет 2 первых объекта списка  — Иванова и Петрова

class[1..2] – вернет последние два (для конкретно этого списка) объекта

Продолжаем погружение :

class[0].surname – получаем значение поля surname у самого первого объекта в списке, то есть получим “Ivanov”

class.name – вернет ВСЕ имена ([Ivan, Petr, Ivan]), то есть поля name у всех объектов списка. Обращаю внимание, что мы не указали никаких индексов, поэтому берется весь список и у каждого элемента ищется поле name. Если у какого-то из объектов такого поля нет — вмеcто него будет null, но в любом случае вернется список – это важно понимать.

class[0].address.house – вернет 12, то есть номер дома Вани Иванова. И так далее, используя индексы и точку можно двигаться вглубь структуры к любому из полей

securityservВажно! Попробуем получить уникальное поле some.value из последнего объекта, навскидку это

class[-1].some.value – ожидали 42, но получаем нулл! Дело в том, что ключ содержит точку и при стандартной записи GPath считает, что должно быть поле some, а в него вложено поле value, не находит его и возвращает нулл. Вот как нужно поступать в таких случаях:

class[-1].’some.value’ – использование апострофа экранирует ключ и подсказывает, что точка в данном случае –часть названия, возвращает искомые 42

Но это все были простые случаи перемещения по иерархии, а в жизни нам чаще всего нужно искать данные порядок которых не известен или меняется. Для этого нужно использовать сложные запросы в стиле

2. Операция {it.поле условие}

class.find {it.name == ‘Ivan’} – вернет нам первый объект в котором есть поле name со значением Ivan, обратите внимание, что имя указано в апострофах, так как это строка, а не число.

securityservВажно! it – это внутренняя переменная, которую генерирует Gpath, как итератор, она поочередно получает значения всех объектов, которые есть на данном уровне. То есть, для примера выше, будут перебираться все объекты-ученики, вложенные в class и у них будет проверяться поле name

class.findAll {it.name == ‘Ivan’} – почти то же самое, но вернется список ВСЕХ объектов, удовлетворяющих условию. Думаю вы легко проведете аналогию с некоторыми методами вебдрайвера – возвращает первый элемент, возвращает все. Условия конечно могут быть не только равенство, но и неравенство «!=» и больше, меньше, все как в Java.

securityservВажно! Строка в Груви – это сравниваемый объект, к ней применимы операторы «>», «<», а также некоторые функции, которые в Java ассоциируются с числами и к строке не применимы!

class.findAll {it.name > ‘Ivan’} — вернет не ошибку, а объект Петрова Пети, потому что имя Petr>Ivan, буква P стоит дальше по алфавиту, чем I.

Если условие в фигурных скобках не указать, оставив лишь поле, то вернутся все элементы, где есть данное поле

class.findAll {it.name} — вернет все объекты, так как условия нет, а поле name есть у каждого

class.findAll {it.’some.value’}  -вернет только Сидорова, так как поле some.value есть только у него

Теперь рассмотрим более интересные функции и запросы.

3. Стандартные математические функции min(), max(), sum()

Применимы только к спискам, содержащим числа или строки, причем сумма для строк вернет конкатенацию

class[0].marks.min() – получаем минимальную оценку первого объекта в списке (Иванов), к сожалению это 2.

class.address.house.max() — получаем максимальный номер дома среди всех учеников, вернет 12 – дом Иванова

securityservК функциям min/max можно также применить выражение {it.поле} для нахождения элемента.

class.max {it.address.house} — вернет объект Иванова, так как у него самый большой номер дома. Важнейшая разница тут в том, что возвращается не само максимальное значение, а объект, который его содержит, что нужно чаще всего.

class.address.street.sum() -вернет конкатенацию названий улиц всех учеников то есть LeninMarxPobedy, так как улицы — это строки

class[-1].marks.sum() — вернет сумму оценок последнего ученика, то есть 14

Попробуем получить сумму всех оценок всех учеников:

class.marks.sum() — вместо ожидаемого числа, получаем список всех оценок [2, 3, 4, 4, 5, 5, 5, 3, 5, 3, 4, 2, 2, 3]! Это происходит из-за того, что class.marks вернет нам список списков или двумерный массив [[2, 3, 4, 4, 5], [5, 5, 3, 5], [3, 4, 2, 2, 3]] и операция суммирования в таком случае просто соберет все элементы в один список. Выходом будет или еще раз применить суммирование или сделать структуру данных плоской, используя flatten() функцию

class.marks.flatten().sum() — вернет ожидаемое число 50 и , на мой взгляд, читаемей, чем аналог «class.marks.sum().sum()»

4. Общие функции

size() – функция, возвращающая размер (количество полей, элементов, букв), применима к строке, списку, объекту. При этом для строки вернет количество букв, для списка количество элементов, для объекта количество полей. Не применимо к числу!

class.size() — ожидаемо вернет 3, то есть количество учеников в списке

class[0].name.size()  — вернет 4, то есть количество букв в слове Ivan(поле name первого элемента)

class[0].address.size() — вернет 2, то есть количество полей в объекте адрес (улица, дом) у первого ученика

unique() – функция, которая возвращает только уникальные элементы, применима только к спискам.

class.name.unique() — вернет список из двух уникальных имен, так как у нас два Ивана [Ivan, Petr]

class.unique() — вернет все 3 наших ученика, так как все они уникальны

class.marks.flatten().unique() — вернет список уникальных оценок [2, 3, 4, 5], например, чтобы убедиться, что никто из ребят не получал 1

5. Всем известные функции работы со строками endsWith, startsWith, contains

class.findAll {it.surname.endsWith(‘rov’)} — получим список всех учеников, у которых фамилия заканчивается на «ров», то есть Петрова и Сидорова

class.findAll {it.name.startsWith(‘I’)} — список всех, у кого имя начинается на I, то есть Сидорова и Иванова (у обоих имя Ivan)

class.findAll {it.name.contains(‘e’)} — вернет того, у кого в имени есть «е», то есть Петра Петрова

securityservВажно! Функция contains применима и к спискам:

class.findAll {it.marks.contains(2)} — вернет нам список двоечников, обратите внимание, что двойка без апострофов, так как оценки -это числа, а не строки

! – в значении НЕ применим к условиям, скажем если мы хотим увидеть кто у нас в классе еще не получал двоек

class.findAll {! it.marks.contains(2)} — вернет только Петрова, обратите внимание на восклицательный знак!

6. Функции преобразования.

Могут быть полезны в редких случаях, так как в основном при работе с json мы читаем данные, а не преобразовываем их. Вообще, как я понимаю тут можно использовать любые функции Груви, но далеко не все они нужны.

reverse() – разворот, применимо к спискам и строкам

class[0].marks.reverse() — вернет [5, 4, 4, 3, 2] то есть перевернутый список оценок первого ученика

class[1].name.reverse() — вернет rteP, то есть перевернутое имя второго ученика

toInteger() – применимо к строкам, возвращает число из строки

class[0].age.toInteger() -вернет 13, то есть возраст первого ученика, но не строкой, а числом. Ниже будет более полезный пример использования данной функции

Collect – функция преобразования данных и сбора их в список,

class.collect {it.name.toUpperCase()} — просто как пример преобразования полученных результатов, вернет все имена из заглавных букв [IVAN, PETR, IVAN]

class.collect {it.age.toInteger()} — вернет список всех возрастов [13, 14, 14], причем это список именно чисел, а не строк! Скажем, если мы хотим найти максимальный возраст

class.collect {it.age.toInteger()}.max() — вернет 14, верный ответ

securityservПочему в данном случае нельзя использовать «class.age.max()»? Потому что в нашем случае это строки, а они сравниваются по алфавиту, а не как числа и если скажем поправить возраст одного из учеников на «4», то максимум вернет именно его, так как 4 стоит в алфавите дальше «1».

Если мы хотим найти самого младшего в классе, с учетом того, что возраст – строка

class.min {it.age.toInteger()} -вернет нам Иванова, которому 13

Еще обращаю внимание, что операции не терминальные, после поиска или преобразования мы можем еще применить функцию или перейти к полю

class.find {it.name.endsWith(‘an’)}.marks[-1] — получаем последнюю оценку первого найденного ученика с именем, заканчивающимся на «an»


Software-Testing.Ru

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s