Создание типобезопасного JSON в Swift

И как это улучшит фиктивные данные ваших модульных тестов.

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

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

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

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

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

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

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

Обратите внимание, что Question и Answer имеют разные поля, поэтому вам нужно связанное значение в случаях Post перечисления. Это означает, что вам нужен собственный синтаксический анализ.

В этом случае нетрудно заставить Post соответствовать Decodable. Вам нужно только переключить поле type и вызвать инициализатор Decodable из Question или Answer.

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

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

Но, как я уже говорил, использование строк JSON подвержено ошибкам и небезопасно. Чтобы улучшить это, вы можете использовать перечисление JSONValue.

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

Я также сделал его совместимым с протоколом Decodable и показал, как вы можете использовать его для анализа общего поля JSON. Если вы пропустили последний пост и хотите взглянуть на него, вы можете найти ссылку внизу этого поста.

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

Вы можете статически построить тестовые примеры с инициализаторами JSONValue:

Теперь он стал намного более читаемым без экранирования всех символов. И он безопасен по типу и синтаксису! Вам не нужно беспокоиться, если вы добавите или опустите JSON символы, такие как : и , , потому что компилятор Swift позаботится об этом за вас.

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

Поскольку ваши тесты получают строки JSON, а теперь у вас есть JSONValue, вам необходимо преобразовать их. Во-первых, приведите JSONValue в соответствие с Encodable протоколом. Для этого переключите JSONValue и для каждого случая кодируйте обернутое значение.

Все заключенные в JSONValue значения равны Encodable, поэтому добавлять код не нужно. Работает "из коробки".

Он по-прежнему не дает вам строки, поэтому вам нужно вызвать JSONEncoder.encode(). Это функция бросания, а это значит, что вам нужно добавить в свои тесты обработку ошибок.

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

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

Передача #file и #line функции fatalError позволяет отображать ошибку в правильном месте вашего теста.

Поскольку компилятор проверяет большую часть синтаксиса, единственный раз, когда я получил fatalError, был, когда я пытался закодировать JSON Fragment. Фрагмент JSON - это JSON, у которого нет Object или Array в качестве корня.

Если вам действительно нужна возможность кодировать фрагменты JSON, вы можете изменить функцию jsonString, чтобы обрабатывать случаи фрагмента другим способом:

Теперь функция кодирует фрагменты JSON как простые строки. Объекты и массивы по-прежнему вызывают JSONEncoder.

Новые тестовые данные - это большое улучшение по сравнению с подходом к необработанным строкам JSON, но он все еще не идеален. Было бы неплохо, если бы вы могли удалить все эти .string, .object, .array и другие случаи перечисления?

Вы можете сделать это! Swift предоставляет способ, использующий ExpressibleByLiteral протоколы. Эти протоколы позволяют назначать буквальное значение настраиваемому типу. Например, вы можете заставить JSONValue соответствовать протоколу ExpressibleByStringLiteral:

extension JSONValue: ExpressibleByStringLiteral {
    public init(stringLiteral value: String) {
        self = .string(value)
    }
}

Затем вы можете инициализировать его статической строкой:

let json: JSONValue = "I am a JSONValue, not a String!"

Выглядит действительно хорошо, правда? Для каждого возможного типа JSON существует один протокол, и вы можете сделать JSONValue соответствующим им всем! Взгляните на приведенный ниже фрагмент:

Теперь вы можете создавать JSONValue из буквальных типов, как если бы вы создавали String, Int, Bool, Double, Array или Dictionary. Если вы удалите все, теперь уже устаревшие, инициализаторы перечисления из предыдущего PostTestCases примера, вы получите:

Какое большое улучшение! Теперь это больше похоже на структуру JSON.

Фактически, это тот же JSON из первого примера, с удаленным экранированием двойных кавычек, все фигурные скобки {} заменены скобками [] и активна "Хорошая печать".

Вы можете вставить любой JSON файл, выполнить простой поиск / замену от фигурных скобок к скобкам и получить типизированные данные для ваших тестов! Woohoo!

В моей сумке есть еще один маленький трюк, который я могу вам показать: добавив крошечное расширение к JSONValue, вы можете декодировать JSONValue в любой Decodable тип!

Теперь вы можете создать любой Decodable тип в форме DSL. Вы можете попробовать это с предыдущими примерами данных:

Довольно круто, правда? 😎

Надеюсь, это новое знание вам поможет. Это, безусловно, помогло мне.

Если вам понравился этот пост, не забудьте хлопнуть 👏 и поделиться им с друзьями 😄.

Вы можете получить доступ к предыдущему сообщению, в котором было представлено перечисление JSONValue, щелкнув ссылку ниже: