Удобный поиск содержимого JSON с помощью одного простого метода

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

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

Хотя доступны существующие решения, такие как jsonpath, я решил принять вызов и создать свое простое решение.

Проблемы

Тестирование динамических страниц/экранов на платформах Android и iOS часто может быть сложной задачей. Есть несколько трудностей, с которыми мы можем столкнуться в процессе тестирования, в том числе:

  • Запуск тестов без предварительного знания о том, присутствуют ли на экране определенные элементы. Это может привести к ненужным ошибкам тестирования и пустой трате времени на проверку ошибок, которая должна была быть необязательной.
  • Различия в языках, используемых в ответах, полученных во время тестирования. Полагаться на жестко закодированный текстовый поиск проблематично, так как это может привести к сбоям теста, когда ожидаемый текст не соответствует фактическому ответу.

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

Возьмем этот тест в качестве примера:

Given the user is on the national animal screen
When he selects the country "England"
Then he should see the "Lion" national animal

Поскольку мы не знаем, будет ли в ответе «Англия», мы можем сначала запросить данные и смоделировать наш тест на основе того, что мы получим. Приведенный выше сценарий будет выглядеть примерно так:

Given the user is on the national animal screen 
When he selects the first country 
Then he should see its national animal

# or

Given the user is on the national animal screen 
When he selects one of the countries
Then he should see its national animal

Решение

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

Первая проблема, которую мы собираемся решить, — это анализ ответа JSON, это можно сделать очень легко, добавив require 'json' в ваш файл и вызвав метод parse.

Вы можете сохранить этот файл в свой проект как test.json:

{
  "content": [ 
    { 
      "type": "group", 
      "payload": { 
        "type": "collapsible", 
        "payload": { 
          "title": "England",
          "rows": { 
            "title": "Lion",
            "description": "The lion is a large cat of the genus Panthera native to Africa and India."
          }
        }
      }
    },
    { 
      "type": "group", 
      "payload": { 
        "type": "collapsible", 
        "payload": { 
          "title": "Scotland",
          "rows":
          {
            "title": "Unicorn",
            "description": "The unicorn is a legendary creature that has been described since antiquity as a beast with a single large, pointed, spiraling horn."
          }
        } 
      } 
    }
  ]
}

Вы также можете написать свой собственный или использовать этот сайт https://jsonplaceholder.typicode.com/users.

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

Давайте создадим новый файл с расширением .rb. Как правило, не рекомендуется выполнять операции синтаксического анализа в рамках инициализации класса. Однако в этом конкретном примере мы сделаем исключение и выполним синтаксический анализ внутри инициализации, чтобы проиллюстрировать концепцию.

require 'json' 

class JsonSearch
  attr_reader :parsed_json

  def initialize(json)
    @parsed_json = json.is_a?(String) ? JSON.parse(json) : json
  end
end

Найти по пути

Чтобы найти значения по пути, мы создадим следующие методы:

def find_by_path(path, data = [@parsed_json])
  mapped_data = []

  path_key = get_first_path_key(path)

  mapped_data << data.map do |element|
    mapped_element = element.select do |i|
      path_key = i.is_a?(String) ? path_key.to_s : path_key.to_sym

      i.eql?(path_key)
    end
    mapped_element[path_key]
  end

  mapped_data = mapped_data.flatten.compact
  path = remove_from_path(path)

  path.empty? ? mapped_data : find_by_path(path, mapped_data)
end

def get_first_path_key(path)
  if path.include?('.')
    path.split('.').first
  elsif path.is_a?(Array)
    path.first
  else
    path
  end
end

def remove_from_path(path)
  if path.is_a?(Array)
    path.drop(1)
  else
    path.split('.').drop(1).join('.')
  end
end

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

Он принимает два параметра. Первый — это путь, по которому мы найдем нужные значения в виде строки (например, ‘content.payload.title’) или массива (например, [‘content’, ‘payload’, ‘titile’]). Вторым будет проанализированный JSON в виде массива, чтобы избежать создания других ненужных массивов при использовании метода .map.

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

Мы также извлекли несколько строк в новые методы, чтобы получить чистый и понятный код.

Большой! Однако важно отметить, что существуют определенные факторы, которые следует учитывать, когда входящие данные либо пусты, либо не соответствуют желаемому формату, о чем мы вскоре поговорим.

Чтобы обеспечить надежность, давайте добавим быструю проверку на наличие пустого или нулевого пути. Кроме того, мы выравниваем данные, поскольку их структура может меняться, и нам нужен согласованный формат для дальнейшей рекурсии.

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

.flatten.compact предназначен для удаления любых нулевых значений, чтобы мы не получили mapped = [nil] или вложенные массивы [[‘a’, ‘b’]].

def find_by_path(path, data = [@parsed_json])
  return [] if path.nil? || path.empty?

  mapped_data = []

  path_key = get_first_path_key(path)

  data.flatten!

  mapped_data << data.map do |element|
    return mapped_data if element.is_a?(String)

    mapped_element = element.select do |i|
      path_key = i.is_a?(String) ? path_key.to_s : path_key.to_sym

      i.eql?(path_key)
    end
    mapped_element[path_key]
  end

  mapped_data = mapped_data.flatten.compact
  path = remove_from_path(path)

  path.empty? ? mapped_data : find_by_path(path, mapped_data)
end

Написав метод, давайте дадим ему некоторое содержимое для поиска. Мы создаем экземпляр класса JsonSearch, даем ему файл JSON и вызываем наш метод с помощью строки.

test_json = File.read(./path/to/your/json.json) 
json_search = JsonSearch(test_json).new
path = 'content.payload.payload.title'

countries = json_search.find_by_path(path)
p countries

# => ['England', 'Scotland']

Большой успех!

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

Заключение

В этой статье мы рассмотрели, как обрабатывать динамические данные при тестировании пользовательского интерфейса, чтобы уменьшить ненадежность тестов и повысить надежность, а также переписали наши тесты, чтобы они соответствовали этим изменениям.

В следующей статье мы обсудим другой метод поиска в файле JSON и переназначения его в соответствии с нашими потребностями.

Спасибо за прочтение.