Когда я впервые столкнулся с написанием тестов для микросервиса, API которого был реализован согласно протоколу JSON-RPC, я осознал что построение качественных проверок для json элементов куда требовательнее, чем я считал ранее.

Ниже приведён простой пример.

От сервиса получен ответ:

{
  "result": 12,
  "id": 46929734,
  "jsonrpc": "2.0"
}

Что требуется проверить:

  1. Ответ содержит элемент «jsonrpc» и его значение равняется «2.0»
  2. Ответ содержит элемент «id» и его значение равняется аналогичному элементу переданному в запросе, в данный момент допустим это — 46929734

Ответ содержит элемент «result» и его значение равняется 12


Как это необходимо проверить, используя Java + TestNG + Gson:

Assert.assertTrue(response.has("result"));
Assert.assertTrue(response.has("jsonrpc"));
Assert.assertTrue(response.has("id"));

Assert.assertTrue(response.get("result").isJsonPrimitive());
Assert.assertTrue(response.get("jsonrpc").isJsonPrimitive());
Assert.assertTrue(response.get("id").isJsonPrimitive());

Assert.assertEquals(response.get("result").getAsInt(), 12);
Assert.assertEquals(response.get("jsonrpc").getAsString(), "2.0");
Assert.assertEquals(response.get("id").getAsInt(), 46929734);

В этот момент, в сознании может начать формироваться вопрос «Зачем так много ассертов?». Да, фактически при тестировании, вас интересует только группа из трех последних ассертов, они проверяют значения json элементов в ответе и если они пройдут — значит ответ соответствует ожиданиям. Но, что если тест упадет? Рассмотрим несколько возможных причин падения такого теста:

  1. Ответ ->
    {
      "result": 12,
      "id": 46929734,
      "jsonrpc": "1.0"
    }

    Ошибка от TestNG -> java.lang.AssertionError: expected [2.0] but found [1.0]
    Ждали «2.0», получили «1.0» — тут все понятно.
  2. Ответ ->
    {
      "result": 12,
      "id": 46929734,
      "jsonrpc": {}
    }

    Ошибка от TestNG -> java.lang.UnsupportedOperationException: JsonObject

    Попытались распарсить как строку элемент, который абсолютно неожиданно получили в ответе как обьект.
  3. Ответ ->

    {
      "result": 12,
      "id": 46929734
    }

    Ошибка от TestNG -> java.lang.NullPointerException
    Попытались распарсить как строку элемент, которого нет в ответе.

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

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

Gassert.verifyInteger(response, "result", 12);
Gassert.verifyString(response, "jsonrpc", "2.0");
Gassert.verifyInteger(response, "id", 46929734);

В библиотеке реализованы методы для проверки:

  • элементов всех примитивных типов, вложенных и нет, с проверкой значения и без
  • JsonObject, вложенных и нет, с проверкой значения и без
  • JsonArray, вложенных и нет, с проверкой значения и без
  • JsonNull элементов, вложенных и нет
  • типов элементов внутри массива
  • содержания массивом ожидаемого элемента
  • размерности массива
  • размерности обьекта

На всех уровнях проверки, реализованы более детализированные ошибки относительно тех что предоставляет TestNG по умолчанию.

Рассмотрим повторно возможные причины падения теста:

  1. Ответ ->
    
    {
      "result": 12,
      "id": 46929734,
      "jsonrpc": "1.0"
    }
    

    Ошибка от Gassert -> java.lang.AssertionError: Element [jsonrpc] verification failed. expected [2.0] but found [1.0]
  2. Ответ ->

    
    {
      "result": 12,
      "id": 46929734,
      "jsonrpc": {}
    }
    

    Ошибка от Gassert -> java.lang.AssertionError: Element [jsonrpc] is not a JsonPrimitive. did not expect to find [true] but found [false]
  3. Ответ ->

    
    {
      "result": 12,
      "id": 46929734
    }
    

    Ошибка от Gassert -> java.lang.AssertionError: Json does not contains element: [jsonrpc]. did not expect to find [true] but found [false]

Также, большинство методов для проверок перегружены с дополнительным строковым параметром, который в случае падения ассерта будет добавлен к тексту выводимой ошибки. Данную возможность можно использовать например для того, чтобы выводить полное тело ответа или запроса+ответа в ошибке.

Библиотека находится в публичном github репозитории, а также добавлена в maven репозиторий.

Maven
Github
APIDocs

Комментарии (3)


  1. Mingun
    18.12.2019 18:01

    Сомневаюсь, что сообщение "did not expect to find [true] but found [false]" сильно понятнее вышеприведенных. Во первых, оно капитанско-очевидное ("не ожидали X, но нашли не X" ~= "не ожидали X, но не нашли X" ~= "ожидали X, но нашли X"), во-вторых, задачу понять, что вместо числа пришел объект оно совершенно не решает.


    1. daymock Автор
      18.12.2019 19:23

      Абсолютно с вами согласен, про капитанскую часть ошибки «did not expect to find [true] but found [false]», но это дефолтный текст, который предоставляет TestNG, в то же время другая часть ошибки «Element [jsonrpc] is not a JsonPrimitive.» — описана мной и поможет сориентироваться в том с каким именно ключем проблема и какого она характера. Если разобрать ситуацию на указанном примере — элемент jsonrpc не соответствует ожидаемому типу JsonPrimitive, при этом его фактический тип можно будет увидеть в ответе на запрос который вы проверяете, или передать проверяемую часть ответа(или весь ответ\или связку запрос-ответ) в параметр для кастомной части ошибки если есть желание вывести максимальное количество информации непосредственно в результате сравнения.


  1. vmm86
    19.12.2019 10:06

    Как вариант, можно проверять содержимое с помощью JSON схем. Разные проверки указываются в схеме разными способами, и в зависимости от ошибки придёт то или иное сообщение об ошибке.
    Есть библиотеки для разных языков. Для Java из доступных на GitHub мне больше всего понравилась разработка Everit, которая поддерживает современные стандарты или драфты JSON схем (не только версию 4, но также 6 и 7).