Применение JSON-Schema в тестировании и документировании API

ks7it 16 июля 2013 в 10:15 26,9k
Справочный API 2ГИС разрабатывается уже 4 года. Появилось около 40 методов, которые возвращают достаточно крупные и иерархически сложные структуры в формате JSON и XML. Совсем недавно я решил поделиться накопленным опытом и выступить на конференции DevConf.
Одна из тем доклада вызвала наибольший интерес у участников — это использование JSON-Schema при тестировании формата выдачи API. В этой статье я расскажу, какие задачи решает этот подход, какие имеет ограничения, что вы получаете из коробки, а что идёт бонусом. Поехали!



JSON-Schema — это JSON-аналог формата XML Schema. Суть его в том, что он в декларативном виде задает структуру документа. Ничто не мешает использовать JSON-схемы для целей тестирования.

Казалось бы, какие могут быть сложности в тестировании API? Это же не какой-нибудь изощренный UI. Подумаешь, выполняем запрос и сравниваем результат с ожидаемым.
Расстраивают громоздкие иерархические структуры, которые могут быть в запросах или ответах. Когда параметров в районе 50-ти, учесть все возможные характерные случаи крайне проблематично.

В дополнение к тому, что API должен выполнять возложенные на него обязанности, ждешь от него продуманности и логичности. В том числе это относится к формату. Хочется, чтобы числа всегда отдавались, как числа, а не строки. Что если массив пустой, он не должен быть равен null, или вообще отсутствовать в ответе, а пусть он будет пустым массивом. Это, конечно, мелочи, но пользователям API очень неприятно о них запинаться. Плюс это может приводить к скрытым ошибкам. Так что строгости формата следует уделять внимание.

В целом тестирование структуры и формата — задача не хитрая. Для каждого метода можно описать структуру запроса и ответа с помощью формата JSON-Schema.
Как говорится, все уже придумано до нас. Следует просто грамотно этим воспользоваться.

Тестирование формата API

Запрос и ответ

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

JSON-Schema

Формат

Значит, поможет нам в нашем нелегком деле JSON-Schema. Краткое введение в сам формат уже есть на хабре. Поэтому ограничимся тривиальным примером. Возьмем JSON-объект:

{ "a":10, "b":"boo" }

Чтобы задать его структуру, тип параметров, обязятельность, достаточно заменить значения параметров на специальные объекты. Смотрим:

{
     "a": {
          "type": "number",
          "required": true
     },
     "b": {
           "type": "string",
           "required": true
     }
}

Согласитесь, вполне наглядно. По большому счету, JSON-Schema стремится быть аналогом формату XML Schema.

PHP-реализация

Конечно, не обязательно при описании ответа использовать именно JSON-Schema. Формат относительно молодой, не до конца устаканился. Не так много библиотек и инструментов, как для формата XML Schema. Но для очень многих разработчиков формат JSON гораздо нагляднее. А если говорить про PHP, то JSON почти родной для него благодаря функциям json_encode и json_decode.

Реализации формата JSON-Schema есть для разных языков. Как видно, выбор не велик. Для PHP две библиотеки от двух институтов: MIT и Berkeley.
На момент написания статьи последний комит в MIT — февраль 2012, а в Berkeley — июнь 2013. Так что, беглый обзор подтолкнул к выбору именно Berkeley.

В качестве ложки дегтя можно отметить, что для объектов в ответе могут присутствовать не только описанные в схеме поля, но и совершенно левые. Причем ответ будет совершенно валидным, что в большинстве случаев не допустимо. Лечится это, на мой взгляд, недоразумение маленьким препроцессором, который явно устанавливает специальное свойство additionalProperties в false по умолчанию.

Валидация

Включать валидацию ответов API на бою особого смысла нет, лишние накладные расходы. А вот когда мы прогоняем тесты — это самое оно. Для переключения режимов достаточно флага в конфигурационном файле приложения.
Если говорить про конкретную библиотеку Berkeley, то пример валидации из тестов:

$validator = new Validator();

$validator->check(json_decode($input), json_decode($schema)); // проверяем $input на соответствие схеме $schema

$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));


Даже интуитивно понятно, что происходит.

Следует заметить, что писать тесты специально для проверки JSON-схемы нет необходимости. Считаем, что на API уже написаны функциональные тесты. Это значит, что если мы включим фильтрацию, то при выполнении тестов автоматически в фоновом режиме будет проверятся формат ответа. Как бы между делом. Никаких специальных усилий кроме, собственно, написания схем прикладывать не нужно.

Да, и небольшое дополнение: для тестов производительности валидацию следует отключить, чтобы не было «наводок». А на машинах разработчиков — наоборот, включить, чтобы быстрее выявлять нарушение формата. Вопрос — кому писать JSON-схемы? У нас пишут разработчики, так удобнее. Схемы живут в коде проекта, и при любых изменениях формата разработчик сразу же корректирует схему.

Документация

С тестированием разобрались, подключаем к юнит-тестам и на выходе получаем уверенность в правильности формата. Но есть ещё один большой бонус при использовании JSON-схемы, к которому мы пришли — это документирование АПИ.

Проблема с документацией одна — ее, блин, надо писать. Документация к API — это ее спецификация, все нюансы должны быть отражены в ней. Ну а как иначе? API же не существует само по себе, он существует только для того, чтобы его клиенты могли с ним работать. И чем меньше сюрпризов от него ждешь, тем он понятнее и пользователи быстрее и комфортнее могут им пользоваться. Может быть, единственный случай, когда на документации можно сэкономить, если разработчики API — одновременно его пользователи. А это бывает далеко не всегда. Поэтому, лучше бы документация была. Ну а если сумарное количество параметров — сотни, как поддерживать документацию актуальной? Мало того, что пишем код с тестами, так еще и приходится за документацией следить. Короче, плюс один «головняк», коих и без документации хватает.

Версионирование

Так, а что насчет версионирования документации? Дело в том, что у нас, как и у многих, принят подход — каждая фича делается в отдельной ветке в git.
Хорошо, если бы каждой ветке соответствовала своя версия документации. Так всем проще: и разработчику, и тестировщику. Сделал задачу в ветке, тут же написал документацию и «забыл» про задачу.

Поэтому хорошо, когда документация живет вместе с кодом. А чтобы еще и было удобно мержить одного и того же документа из разных задач, логично хранить ее в неком текстовом формате. Можно хранить ее в формате Markdown, семантических bb-кодов или еще как-нибудь. Но по факту это все равно остается почти голый текст, с различными таблицами, связями между ними, перекрестными ссылками и т.п. Не совсем понятно, как тестировать корректность документации. Каждый раз внимательно все проверять вручную, “задалбливаешься”. Это во-первых, а во-вторых, ошибки все равно остаются.

JSON-Schema

Ну, а все-таки:



Отказаться от нее полностью нельзя, но можно сильно сократить количество усилий для ее поддержки. У нас уже есть JSON-Schema. В ней уже прописаны все названия параметров в ответе API, иерархические связи между ними, типы данных, обязательность параметра. А именно эта информация также нужна для генерации документации.

Поэтому мы решили скрестить бульдога с носорогом и таки писать документацию прямо в JSON-Schema. В формате JSON-Schema уже предусмотрен тег description. Но он должен быть строкой, согласно спецификации. А было бы неплохо еще и примеры добавить (вложенный тег examples), и опции какие-нибудь специфические, типа секретный параметр (вложенные тег hide) и прочее. Поэтому тег-объект лучше для этого подойдет. Мы выбрали название тега — «meta». Пример:

"reviews_count": { 
    "type": "number", 
    "required": true, 
    "meta": { 
        "description": "Количество отзывов", 
        "hide": true 
    } 
}


Представление

Теперь натравливаем нашу JSON-схему на спец. парсер, и она превращается в элегантную документацию. При формировании документации учитывается не только содержимое нашего собственного тега «meta», но и информация из нативных тегов схемы.

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

Для максимальной гибкости документация не должна строиться полностью из JSON-схемы. У нас хорошо зарекомендовал себя подход, когда для каждой страницы документации есть отдельный текстовый файл. А уже в него вставляется ссылка на конкретную JSON-схему. При сборке страницы JSON-схема преобразуется в финальный текстовый вид. Таким образом, мы решаем проблему размещения в документации произвольного текста, примеров, и прочих материалов. И при этом мы не пытаемся все, что можно, запихнуть в JSON-схему.

Самотестирование

Получается, что одни и те же JSON-схемы используются и для тестов и для документации. А это значит, что JSON-схемы всегда актуальны и корректны, иначе тесты свалятся. Так что в документации в принципе не может быть ошибок, связанных с названиями параметров, их типов, обязательности/необязательности, списка допустимых значений, иерархических связей между параметрами. Что, согласитесь, уже не мало.

Примеры

Документация без примеров использования сильно увеличивает порог вхождения для пользователей API. Поэтому их следует добавлять в обязательном порядке. Мы их организуем следующим образом. Описываем в документации запрос и рядом пример ответа.

Но, как не трудно догадаться, всплывает проблема актуальности текста ответа. Пройдет неделя, формат расширится, и что, примеры заново составлять? Есть выход лучше.

Пришли к тому, что результаты запросов в примерах документации должны быть динамическими. Тут возможны разные подходы. Так как у нас ответы API часто очень крупные, то мы их показываем в документации по клику на определенную область. Именно в этот момент мы и выполняем запрос. Самая простая схема, но с небольшой задержкой получения данных.

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

Кстати, раз уж примеры все равно пишем, то их тоже можно считать разновидностью функциональных тестов. Ну правда, собираем все запросы в документации, включаем JSON-схему и проверяем валидность. Вместе с тем, таким образом можно определить сломанные методы или неправильно составленные запросы.

Пустые значения

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

Параметр в любом случае должны присутствовать в ответе, даже если он содержит пустое значение. Так лучше видна структура ответа. Отпадает необходимость обращаться к документации только для того, чтобы ее узнать.

Параметр типа массив возвращает пустой массив. Объект — null. Для числовых и строковых параметров, если ноль и пустая строка осмысленные значения, то их и возвращаем. Например, параметр «количество отзывов» вполне может возвращать ноль — это логично. А вот параметр «этажность здания» если вернет ноль — это бред. Если, допустим, для определенного дома неизвестна его этажность, следует вернуть null.
Где null возможен, а где нет, явно указывается в JSON-схеме. А значит, и в документации. Ваш подход может отличаться, главное, чтобы он был единообразен для всех параметров, иначе конечным пользователям API добавится головной боли.

Заключение

JSON-Schema сильно экономит драгоценное время на тестирование и документирование API. Это как раз тот случай, когда небольшое количество усилий приносит много профита. И чем крупнее API, тем больше усилий этот подход позволяет сэкономить.

Правда, один раз придется вложиться в написание небольшого инструментария для использования JSON-схем.

Помимо экономии времени на тесты, проще поддерживать обратную совместимость в API, т.к. формат API наглядно выражается в JSON-схеме. А тяжелый груз документации становится легкой ручной кладью.
Проголосовать:
+42
Сохранить: