Как тестировать не-REST-бекэнд. Часть первая, GraphQL
Привет! Меня зовут Сергей, я более 11 лет в тестировании, и успел за это время перепробовать множество разных подходов в QA — начинал простым тестировщиком, затем строил и развивал всевозможные отделы тестирования и автоматизации, а сейчас работаю в QIWI.
В этой серии постов я хочу поговорить с вами про тестирование трех популярных так называемых не-REST-бэкендов. Самое главное для начала — определиться с терминами, договоримся, что везде в тексте, где я упоминаю REST — речь идет именно о REST HTTP-бэкенде. Наверняка многие из вас с ним работали и вообще неплохо знакомы.
Но есть ещё три других, собственно, не-REST-бэкенда. С ними тоже полезно научиться работать: во-первых, для общего развития, во-вторых, будете знать, как подступаться к их тестированию, на случай, если ваша команда вдруг решит поработать на одном из них.
Под катом — разбор тестирования первого из этой тройки, GraphQL. Все примеры в посте я делал с помощью Postman, он достаточно популярен и доступен, чтобы вы при желании могли всё быстро в нём повторить.
Немного базы. Я буду сравнивать тестирование GraphQL с HTTP, поэтому сначала — небольшая вводная. Что такое HTTP? Это протокол передачи гипертекста, включающий в себя две вещи — отправка запросов и получение ответов. Самый простой запрос в HTTP состоит из стартовой строки, типа запроса, заголовка запроса и его тела. В теле передаются данные (обычно в виде JSON), заголовки несут чисто информационный характер, поэтому не будем на них заострять внимание.
А вот с типом запроса поработаем.
Прежде всего, взглянем на тело запроса через призму CRUD. Это четыре основных операции над информацией:
Create (создание)
Read (чтение)
Update (обновление)
Delete (удаление)
Для каждого этого типа операции в REST HTTP существует свой тип запроса:
Для создания это POST или PUT.
Для чтения — GET.
Для апдейта — PUT или PATCH.
Для DELETE — DELETE.
Тут могут быть отличия, так как, повторюсь, это рекомендации, и конечная реализация зависит от конкретного разработчика.
Итак, мы сформировали запрос и отправили его. Что мы получили в ответ? В ответ мы получаем информацию о том, какая строка нами запрашивалась, какие были заголовки в ответе, тело с данными, если есть, и код ответа.
Коды ответа делятся на 5 групп:
100 — информационные
200 — успешные
300 — редиректы
400 — ошибки клиента
500 — ошибки сервера.
Это основные штуки, которые стоит знать в разрезе поста про REST HTTP.
А теперь поговорим про GraphQL.
Что такое GraphQL
Это язык запроса для вашего API и среда на стороне сервера для выполнения запросов с использованием системы типов, которую вы определяете для своих данных. Здесь главное выделить две вещи. Во-первых, это язык запросов. Во-вторых, он использует систему типов. Вокруг этой пары особенностей всё и крутится.
Предположим, что у вас есть некий REST-сервис, который предоставляет определенную информацию об игроках, матчах, командах и прочих спортивных данных. Думаю, вы все видели REST HTTP-сервис вот такого формата.
У такого формата есть два минуса.
Первый называется overfetching, это ситуация, при которой endpoint возвращает в ответе огромное количество информации, которая вам может быть не нужна. Допустим, вам нужно было какое-то одно значение свойства, а сервис вернул вам в ответе всё, что надо и не надо. Например, мы хотим вернуть только название команды. Но endpoint так спроектирован, что возвращает и историю игр, и рейтинги, и вообще всё, что связано с командой.
Второй минус — обратная ситуация, underfetching, когда endpoint возвращает недостаточное количество информации, и вам приходится делать несколько запросов, чтобы собрать полную картину с данными. И тем самым вы увеличите нагрузку на сервис.
Для решения этих проблем GraphQL предлагает единую точку входа, один endpoint, через который вы можете запрашивать любые нужные вам данные, при условии, что они связаны в схеме.
Схема — это SDL, schema definition language, язык, который может описывать данные и возвращать их сервису, определенным образом проставляя связи между этими данными.
Для примера представьте сервис книжного магазина. Он предоставляет информацию о том, какие в базе есть книги, названия этих книг, какие есть авторы и информацию по ним. Можно посмотреть имя автора и, например, узнать, какие книги он написал.
Чтобы описать его в SDL, достаточно просто несколько строчек.
Ключевое слово query говорит, что сервис может вернуть информацию по книгам, и нам возвращается массив с типом book. Или может вернуть информацию по авторам, и нам будет возвращаться массив с типом author. При этом в типе book у нас есть название книги и ссылка на автора. А у типа author есть его имя и список его книг, которые он написал.
Все, таким образом мы только что определили, какие данные мы можем запрашивать через GraphQL Endpoint, и как они связаны между собой.
Давайте посмотрим, как будет выглядеть запрос на такой книжный сервис.
С помощью этого запроса мы хотим получить информацию о книгах, и тут нас интересуют только названия книг. А ещё хотим информацию по авторам — только их имена.
Сервис после выполнения такого запроса пришлет нам вот такой JSON-объект.
В нём есть служебное слово data, а внутри будут книги, которые мы запросили. Как мы помним, сервис возвращает нам массив с типом book, но у каждого массива этих объектов мы запрашивали только title.
Поэтому сервис вернул массив книг, в которых будут перечислены только их имена. То же самое с автором — вернулся массив авторов, у которых будут в качестве объектов их имена.
В общем, что запросили — то и получили.
В начале работы с GraphQL часто возникает вопрос —, а где вообще посмотреть, что в целом я могу у такого сервиса запрашивать? Ведь метод один, а данных может быть сколько угодно.
Ответов тут сразу три.
Первый — посмотрите саму схему (SDL), в ней сразу видно, что можно запросить у сервиса, какой тип и прочее. Если она достаточно простая, вы будете быстро её читать, никаких проблем не будет.
Второй — для любителей документоориентированного формата. У GraphQL есть плагины, которые позволяют ему с помощью схемы нарисовать на самом деле очень подробную документацию, близкую к swagger, в которой и будет вся исчерпывающая информацию.
Третий — механизм автофетча, который автоматически реализован в POSTMAN.
Давайте перейдем к практике и поработаем с сервисом https://rickandmortyapi.com/. Это очень занятная штука — авторы проекта сделали сервис, который предоставляет разнообразную информацию по мультсериалу «Рик и Морти». Есть не только REST-эндпойнты, но и GraphQL. Он то нам и интересен.
Итак, чтобы сделать GraphQL-запрос , заходим в Postman, New — GraphQL request.
Вводим в адресную строку эндопинт https://rickandmortyapi.com/graphql. Postman автоматически отобразит следующую информацию:
Это тот самый механизм auto fetch. Получается, что Postman сходил на URL GraphQL и попросил его вернуть систему типов и связей, чтобы предоставить нам информацию.
Теперь мы можем спокойно формировать запрос. Например, вот как запросить информацию по персонажу. Для этого нужно определить, служебное слово query и определить все объекты и их свойства, которые хотим получить. Например, запросим всех персонажей, при этом по каждому персонажу хотим получить его имя и картинку.
Отправим запрос и в теле укажем:
query {
characters {
results {
name,
image
}
}
}
Ответ
{
"data": {
"characters": {
"results": [
{
"name": "Rick Sanchez",
"image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg"
},
{
"name": "Morty Smith",
"image": "https://rickandmortyapi.com/api/character/avatar/2.jpeg"
},
{
"name": "Summer Smith",
"image": "https://rickandmortyapi.com/api/character/avatar/3.jpeg"
},
{
"name": "Beth Smith",
"image": "https://rickandmortyapi.com/api/character/avatar/4.jpeg"
},
{
"name": "Jerry Smith",
"image": "https://rickandmortyapi.com/api/character/avatar/5.jpeg"
},
{
"name": "Abadango Cluster Princess",
"image": "https://rickandmortyapi.com/api/character/avatar/6.jpeg"
},
{
"name": "Abradolf Lincler",
"image": "https://rickandmortyapi.com/api/character/avatar/7.jpeg"
},
{
"name": "Adjudicator Rick",
"image": "https://rickandmortyapi.com/api/character/avatar/8.jpeg"
},
{
"name": "Agency Director",
"image": "https://rickandmortyapi.com/api/character/avatar/9.jpeg"
},
{
"name": "Alan Rails",
"image": "https://rickandmortyapi.com/api/character/avatar/10.jpeg"
},
{
"name": "Albert Einstein",
"image": "https://rickandmortyapi.com/api/character/avatar/11.jpeg"
},
{
"name": "Alexander",
"image": "https://rickandmortyapi.com/api/character/avatar/12.jpeg"
},
{
"name": "Alien Googah",
"image": "https://rickandmortyapi.com/api/character/avatar/13.jpeg"
},
{
"name": "Alien Morty",
"image": "https://rickandmortyapi.com/api/character/avatar/14.jpeg"
},
{
"name": "Alien Rick",
"image": "https://rickandmortyapi.com/api/character/avatar/15.jpeg"
},
{
"name": "Amish Cyborg",
"image": "https://rickandmortyapi.com/api/character/avatar/16.jpeg"
},
{
"name": "Annie",
"image": "https://rickandmortyapi.com/api/character/avatar/17.jpeg"
},
{
"name": "Antenna Morty",
"image": "https://rickandmortyapi.com/api/character/avatar/18.jpeg"
},
{
"name": "Antenna Rick",
"image": "https://rickandmortyapi.com/api/character/avatar/19.jpeg"
},
{
"name": "Ants in my Eyes Johnson",
"image": "https://rickandmortyapi.com/api/character/avatar/20.jpeg"
}
]
}
}
}
Получаем объект data, в котором есть объект персонажи (characters), а внутри него как раз результаты в виде массива c объектами, состоящие из имени и картинки
Таким же образом можно запросить что угодно — имя любого персонажа, его изображение, список эпизодов, в которых он появляется, с названиями этих серий, в общем, по желанию.
Запросы можно параметризировать, например, если в будущем вы подумываете об использовании автотестов и о том, чтобы загнать сюда какие-то другие данные. GraphQL позволяет это сделать так:
Обратите внимание на поле variables.
Это поле как раз служит для того, чтобы определять нужные вам переменные. Например, делаем переменную testid, передадим двойку.
{
"test_id": 2
}
Этот testid мы передадим, как параметр через знак доллара (тут стоит заметить, что это не моя вольность, а возможность, определенная разработчиками SDL этого ресурса).
query {
character(id: $test_id) {
name,
image,
episode {
name
}
}
}
Postman сразу выделит данные красным — он не понимает сходу, что это за тип и как его использовать.
Для объяснения нам надо дать имя этой функции. Допустим, getSomeInfo, получение какой-то информации. И надо определить, какой тип параметров здесь служит — укажем, что testid, который мы сюда передаем, будет иметь идентификатор (восклицательный знак значит, что параметр обязательный).
query getSomeInfo($test_id: ID!) {
character(id: $test_id) {
name,
image,
episode {
name
}
}
}
Выполним запрос и получим ответ.
Ответ
{
"data": {
"character": {
"name": "Morty Smith",
"image": "https://rickandmortyapi.com/api/character/avatar/2.jpeg",
"episode": [
{
"name": "Pilot"
},
{
"name": "Lawnmower Dog"
},
{
"name": "Anatomy Park"
},
{
"name": "M. Night Shaym-Aliens!"
},
{
"name": "Meeseeks and Destroy"
},
{
"name": "Rick Potion #9"
},
{
"name": "Raising Gazorpazorp"
},
{
"name": "Rixty Minutes"
},
{
"name": "Something Ricked This Way Comes"
},
{
"name": "Close Rick-counters of the Rick Kind"
},
{
"name": "Ricksy Business"
},
{
"name": "A Rickle in Time"
},
{
"name": "Mortynight Run"
},
{
"name": "Auto Erotic Assimilation"
},
{
"name": "Total Rickall"
},
{
"name": "Get Schwifty"
},
{
"name": "The Ricks Must Be Crazy"
},
{
"name": "Big Trouble in Little Sanchez"
},
{
"name": "Interdimensional Cable 2: Tempting Fate"
},
{
"name": "Look Who's Purging Now"
},
{
"name": "The Wedding Squanchers"
},
{
"name": "The Rickshank Rickdemption"
},
{
"name": "Rickmancing the Stone"
},
{
"name": "Pickle Rick"
},
{
"name": "Vindicators 3: The Return of Worldender"
},
{
"name": "The Whirly Dirly Conspiracy"
},
{
"name": "Rest and Ricklaxation"
},
{
"name": "The Ricklantis Mixup"
},
{
"name": "Morty's Mind Blowers"
},
{
"name": "The ABC's of Beth"
},
{
"name": "The Rickchurian Mortydate"
},
{
"name": "Edge of Tomorty: Rick, Die, Rickpeat"
},
{
"name": "The Old Man and the Seat"
},
{
"name": "One Crew Over the Crewcoo's Morty"
},
{
"name": "Claw and Hoarder: Special Ricktim's Morty"
},
{
"name": "Rattlestar Ricklactica"
},
{
"name": "Never Ricking Morty"
},
{
"name": "Promortyus"
},
{
"name": "The Vat of Acid Episode"
},
{
"name": "Childrick of Mort"
},
{
"name": "Star Mort: Rickturn of the Jerri"
},
{
"name": "Mort Dinner Rick Andre"
},
{
"name": "Mortyplicity"
},
{
"name": "A Rickconvenient Mort"
},
{
"name": "Rickdependence Spray"
},
{
"name": "Amortycan Grickfitti"
},
{
"name": "Rick & Morty's Thanksploitation Spectacular"
},
{
"name": "Gotron Jerrysis Rickvangelion"
},
{
"name": "Rickternal Friendshine of the Spotless Mort"
},
{
"name": "Forgetting Sarick Mortshall"
},
{
"name": "Rickmurai Jack"
}
]
}
}
}
Теперь еще больше раскрутим мощность GraphQL и сделаем следующую штуку — запросим данные сразу из двух разных эндпоинтов. Если посмотреть документацию сервиса по REST, то увидим, что для получения персонажей и эпизодов мы должны были бы сделать два разных запроса. Здесь мы справимся за один.
Запросим информацию о том, в каких эпизодах играл Морти, и одновременно запросим у первого эпизода, какие персонажи в нем играли, и посмотрим, сходятся данные или нет
Сделаем такой запрос:
query {
characters(filter: {name: "Morty Smith"}) {
results {
name,
episode {
name
}
}
}
episodes(filter: {name: "Pilot"}) {
results {
characters {
name
}
}
}
}
Ответ
{
"data": {
"characters": {
"results": [
{
"name": "Morty Smith",
"episode": [
{
"name": "Pilot"
},
{
"name": "Lawnmower Dog"
},
{
"name": "Anatomy Park"
},
{
"name": "M. Night Shaym-Aliens!"
},
{
"name": "Meeseeks and Destroy"
},
{
"name": "Rick Potion #9"
},
{
"name": "Raising Gazorpazorp"
},
{
"name": "Rixty Minutes"
},
{
"name": "Something Ricked This Way Comes"
},
{
"name": "Close Rick-counters of the Rick Kind"
},
{
"name": "Ricksy Business"
},
{
"name": "A Rickle in Time"
},
{
"name": "Mortynight Run"
},
{
"name": "Auto Erotic Assimilation"
},
{
"name": "Total Rickall"
},
{
"name": "Get Schwifty"
},
{
"name": "The Ricks Must Be Crazy"
},
{
"name": "Big Trouble in Little Sanchez"
},
{
"name": "Interdimensional Cable 2: Tempting Fate"
},
{
"name": "Look Who's Purging Now"
},
{
"name": "The Wedding Squanchers"
},
{
"name": "The Rickshank Rickdemption"
},
{
"name": "Rickmancing the Stone"
},
{
"name": "Pickle Rick"
},
{
"name": "Vindicators 3: The Return of Worldender"
},
{
"name": "The Whirly Dirly Conspiracy"
},
{
"name": "Rest and Ricklaxation"
},
{
"name": "The Ricklantis Mixup"
},
{
"name": "Morty's Mind Blowers"
},
{
"name": "The ABC's of Beth"
},
{
"name": "The Rickchurian Mortydate"
},
{
"name": "Edge of Tomorty: Rick, Die, Rickpeat"
},
{
"name": "The Old Man and the Seat"
},
{
"name": "One Crew Over the Crewcoo's Morty"
},
{
"name": "Claw and Hoarder: Special Ricktim's Morty"
},
{
"name": "Rattlestar Ricklactica"
},
{
"name": "Never Ricking Morty"
},
{
"name": "Promortyus"
},
{
"name": "The Vat of Acid Episode"
},
{
"name": "Childrick of Mort"
},
{
"name": "Star Mort: Rickturn of the Jerri"
},
{
"name": "Mort Dinner Rick Andre"
},
{
"name": "Mortyplicity"
},
{
"name": "A Rickconvenient Mort"
},
{
"name": "Rickdependence Spray"
},
{
"name": "Amortycan Grickfitti"
},
{
"name": "Rick & Morty's Thanksploitation Spectacular"
},
{
"name": "Gotron Jerrysis Rickvangelion"
},
{
"name": "Rickternal Friendshine of the Spotless Mort"
},
{
"name": "Forgetting Sarick Mortshall"
},
{
"name": "Rickmurai Jack"
}
]
},
{
"name": "Morty Smith",
"episode": [
{
"name": "Close Rick-counters of the Rick Kind"
}
]
},
{
"name": "Morty Smith",
"episode": [
{
"name": "Rick Potion #9"
}
]
},
{
"name": "Morty Smith",
"episode": [
{
"name": "Never Ricking Morty"
}
]
}
]
},
"episodes": {
"results": [
{
"characters": [
{
"name": "Rick Sanchez"
},
{
"name": "Morty Smith"
},
{
"name": "Bepisian"
},
{
"name": "Beth Smith"
},
{
"name": "Canklanker Thom"
},
{
"name": "Davin"
},
{
"name": "Frank Palicky"
},
{
"name": "Glenn"
},
{
"name": "Hookah Alien"
},
{
"name": "Jerry Smith"
},
{
"name": "Jessica"
},
{
"name": "Jessica's Friend"
},
{
"name": "Mr. Goldenfold"
},
{
"name": "Mrs. Sanchez"
},
{
"name": "Principal Vagina"
},
{
"name": "Summer Smith"
},
{
"name": "Davin"
},
{
"name": "Greebybobe"
},
{
"name": "Pripudlian"
}
]
}
]
}
}
}
Видим, что Морти играл в эпизоде с именем «Pilot».
"data": {
"characters": {
"results": [
{
"name": "Morty Smith",
"episode": [
{
"name": "Pilot"
},
И сразу видим, что в эпизоде «Pilot» среди персонажей есть он.
"episodes": {
"results": [
{
"characters": [
{
"name": "Rick Sanchez"
},
{
"name": "Morty Smith"
},
В итоге в одном запросе мы сделали запрос на два разных endpoint, получили информацию. Это очень полезная фишка GraphQL.
А теперь расскажу немного про особенности GraphQL, которые интересны будут тестировщику.
О чем стоит помнить
Во-первых, так как GraphQL это не протокол, а язык, все запросы — это POST (т.к. нам важно, чтобы было тело в запросе). Даже если вы пошлете put, patch, delete, неважно — он все это дело пропустит и не обратит внимание на тип. Соответственно, возникает вопрос, хорошо — как тут работает наша аббревиатура CRUD?
Для этого у GraphQL есть два служебного слова — это query и mutation.Для всех запросов на чтение GraphQL просит вас указывать слово query, служебный параметр. А если вы хотите что-то поменять (создание, обновление, удаление), вы должны указывать ключевое слово mutation. Это позволяет комбинировать в одном запросе сразу операции создания и чтения
Это очень-очень гибкий язык.
Во-вторых, код ответа почти всегда 200. Причина та же:-) Бывают, конечно, что встречаются 500-ошибки, если сервис ваш упал, но в этом случае просто до обработчиков в GraphQL мы даже не дошли, и 400-е, когда синтаксис неправильный. Но на самом деле GraphQL будет большую часть времени возвращать вам просто 200. Поэтому тестировать коды ошибок здесь, как в REST HTTP, смысла не имеет.
В-третьих, когда я начинал во всем этом разбираться в свое время, я думал — как я все это перетестирую? Все возможные комбинации? Ответ здесь простой — не надо тестировать все. Тестируйте что-то только исходя из бизнес-фичи. То есть если у вас есть бэкэнд, который что-то там отвечает на такой-то запрос клиента, вот и проверяйте, что он будет именно вот это отвечать, а не все возможные переборы параметров, которые он мог бы в теории вернуть. Исчерпывающее тестирование тут не нужно.
В-четвертых. Я писал, что вы можете взять и сделать запрос ко многим endpoints. Это удобно использовать для проверки ролей и доступов. Допустим, у вас есть школьный портал и три роли — учитель, ученик и администратор. Ученику доступны одни разделы портала доступны, учителю чуть больше функционала. Админ вообще может все.
И вот если бы вы тестировали REST HTTP, вы бы выполняли несколько запросов на разные эндпоинты сначала под учеником, потом под учителем, потом под админом. Смотрели бы, где ошибки вернулись, а где нет.
В GraphQL вы можете взять и сделать один запрос, например, под учеником. Запросить данные из всех разделов сайта, которые только есть. И в тех разделах, в которые у ученика есть доступ, он получит данные. А на те, где доступа нет — получит ошибку. Например, «неавторизованный доступ» или «у вас нет прав».
Так вы можете очень быстро в одном грамотно построенном запросе получить много полезной информации. А еще и формат ошибок проверить ;-).
Минусы GraphQL
Прежде всего — обратная сторона, его плюса. Если вам вдруг все-таки надо получить вот эту вот overfetching-информацию, то есть всю информацию, которая есть — это не получится сделать. ам придется явно указывать, что вы хотите получить. GraphQL вас обязывает указывать конкретный запрос. Да-да, если вы хотите увидеть всю информацию по объекту и у объекта будет 500 свойств, вот 500 свойств и надо будет запросить.
Итак, подведем краткий итог
GraphQL — что тестируем?
Во-первых, нужно проверять формат данных, который возвращается. Он же возвращает JSON с определенной структурой. Эта структура парсится клиентам или кем-то еще. Поэтому этот формат структуры нужно проверять — мало ли, что-то изменилось, это может быть сюрпризом. При этом не забываем и про данные, которые лежат внутри. Не лишним будет проверить и их, потому что вдруг он вам вернет нерелевантные данные. Обманывать нехорошо.
Во-вторых, модель ролей и ошибок. В одном запросе можно делать несколько проверок, получая данные из нескольких источников. Например, под определенными правами запросить кучу информации и проверить, куда есть доступ, а куда нет. И все в одном запросе. Очень удобно
В-третьих, тестируйте пользовательский сценарий. GraphQL гибкий и классный, да. Но, допустим, вы создаете пользователя. Как это под капотом происходит? Вы взяли, собрали информацию и распихали ее по разным базам данных, по таблицам, как вам угодно.
А потом заходите в личный кабинет, чтобы прочитать информацию о пользователе, и вам нужно эту информацию вернуть. Эта информация собирается из этих опять баз данных, из таблиц, делается сложный join. Потом оттуда эта выборка каким-то образом собирается, агрегируется, выдается вам наружу. И вот здесь очень много мест, где могут быть баги.
Итак, первая часть цикла закончена, с GraphQL мы разобрались, в следующем посте рассмотрим WebSocket.