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

    Справочный 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-схеме. А тяжелый груз документации становится легкой ручной кладью.
    2ГИС
    147.32
    Карта города и справочник предприятий
    Share post

    Comments 18

      –16
      Как и сказал habrahabr.ru/post/185140/#comment_6438338. Теперь чищу монитор и клаву.
        0
        Проявите уважение, Боромир погиб смертью храбрых!
        0
        Спасибо за статью.
        — как я понял, вы создаете json-schema во время написания тестов, тоесть поддержка актуальности самой схемы напрямую зависит от того, на сколько хорошо поддерживаются тесты?
        — а невозникало мысли генерировать json-schema из валидаторов запросов?
          +1
          Json-схема создается после/до написания функционала, она сама по себе — функциональный тест.
          По ней как раз происходит валидация данных и по ней же строится документация (Объект такой-то содержит поля такие-то и включает объекты такие-то)

          Эдакая круговая порука:
          — Если после изменений кода валится валидация — проверь свой код.
          — Если изменил формат выдачи — поменяй схему, а то валидацию не пройдет и дока устареет.

          +2
          После доклада внедрил JSON-schema у нас в API и сделал тесты в Jenkins. Кстати, помогло найти несколько багов в основном коде и выявить невалидные данные в поиске. Так что спасибо за доклад. И привет вашему начальнику тестирования.
            0
            Буквально неделю назад прикрутили у себя валидацию данных, приходящих с клиентских приложений на сервер, с помощью JSON-schema. Сделали логирование ошибок в данных в сентри, оказалось более 10 ошибок в несложном объекте. Короче, крайне рекомендую.
            0
            Не могу не порекомендовать отличную JS библиотеку валидации, на которую сам недавно наткнулся:
            github.com/geraintluff/tv4

            Работает и на клиенте и на сервере.
            Активно развивается.

            Правда поддерживает только 4 версию «JSON Schema».
              +1
              Реализации формата JSON-Schema есть для разных языков. Как видно, выбор не велик. Для PHP две библиотеки от двух институтов: MIT и Berkeley.

              Там лицензии BSD и MIT, а не творчество Berkley и MIT. Одно «Copyright © 2008, Gradua Networks; Author: Bruno Prieto Reis», другое «Copyright © 2011 Harold Asbridge». Почему на сайте JSON Schema вместо «BSD» написали «Berkley» — вопрос к ним.
                0
                Сергей, спасибо за статью.

                И сразу вопрос.
                В документации к всевозможным API, известное дело, присутствует не только информация об ответе, но и информация о запросе. Запрос (URI, тип, параметры, ...) вы тоже с помощью JSON Schema описываете, или как-то иначе?
                И, да, хотелось бы увидеть (если это не секрет) пример схемы для реального запроса.

                Что касается библиотек и поддержки языков, могу заметить, что реализовать свой собственный валидатор JSON Schema (возможно, включив в него какие-то специфические штуки) не составляет особого труда. Так что, пожалуй, используя почти любой язык, можно смело брать JSON Schema как полезную штуку для документации/валидации/еще каких-то вещей.
                  +1
                  JSON схемы мы используем только для ответов API. И только потому, что API запросы у нас очень простые. В них нет иерархических структур. Валидация происходит на стороне приложения. А для документирования запросов через JSON схемы просто руки не доходят, профита будет не очень много.

                  На счет собственной реализации согласен. Принципальных сложностей в создании валидатора JSON схем нет. Было бы время и желание.
                    0
                    Ага! А что насчет реального примера? ;)
                    Интересует как раз кейс, когда параметр в ответе может принимать, например, и целочисленные значения, и null. Как это будет выглядеть в схеме?
                      +1
                      Перечисление типов никто не отменял:
                      {
                          "type": ["string", "null"]
                          ...
                      }
                      

                      Или для объектов:
                      {
                          "type": ["object", "null"],
                          "header": "Какой-то вложенный объект",
                          "required": true,
                          "properties": {
                              "id": {
                                  "type": "integer",
                                  "required": true,
                               },
                             ...
                          }
                      }
                      

                      В последнем примере либо поле существует и null, либо в нем лежит какая-то структура, которая будет валидироваться согласно properties
                        0
                        Ага, спасибо. У меня почему-то валидатор (jsv4-php) падал при попытке так написать. Но, видимо, дело было в чем-то другом :)
                    0
                    А почему бы не опубликовать JSON schema вместе с API? Это сослужило бы пользу разработчикам, его использующим.

                    Only users with full accounts can post comments. Log in, please.