Используйте возможности интеграционного тестирования в своих приложениях для iOS

Что такое интеграционное тестирование?

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

Почему интеграционное тестирование?

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

Какова основная идея?

Основная идея состоит в том, чтобы полностью контролировать ответ (data), код состояния (HTTPURLResponse) и ошибку (error), которые отправляются вверх с сетевого уровня. Управляя этими параметрами, мы можем имитировать различные сценарии и проверять, работают ли наша логика анализа данных (ответ декодирования), логика бизнес-уровня (View model или Presenter) и все другие модули, как и ожидалось.

Мы также можем проверить сетевые запросы, сделанные нашему экземпляру URLSession нашим сетевым уровнем, и проверить, соответствуют ли они ожиданиям.

Отказ от ответственности: я намеренно избегаю рекомендаций, чтобы код в этой статье был простым.

Что такое URL-протокол?

Здесь мы используем общий экземпляр URLSession для совершения сетевого вызова. Обычно система берет на себя выполнение этого сетевого вызова и возвращает результат.

Но система также дает вам возможность выполнить запрос самостоятельно и вернуть результат. Использование URLProtocol - это способ сделать это.

Давайте начнем:

  1. Создайте URLProtocol subclass и реализуйте приведенные выше обязательные методы.
  2. Зарегистрируйте свой URLProtocol subclass в системе в методе application (_:didFinishLaunchingWithOptions:) вашего AppDelegate.
  3. Назначьте тип вашего подкласса URLProtocol свойству protocolClasses свойства URLSession. Не создавайте экземпляр вашего подкласса, так как система позаботится об этом автоматически.

Теперь всякий раз, когда URLSession запрашивается сетевой запрос, он сначала проверяет объекты в массиве protocolClasses и спрашивает, хотят ли они обработать запрос, вызывая canInit(with: request) — > Bool. Вернув true из нашего подкласса, мы получим возможность обработать запрос.

Наш пример структуры приложения

У нас есть приложение, в котором пользователь может видеть список сообщений [String]. Назовем это пользовательской лентой. Здесь у нас есть три модуля:

1. Сетевой уровень

Он использует общий экземпляр URLSession для выполнения сетевых запросов и декодирует ответ с помощью JSONDecoder.

2. Просмотр модели

Он поддерживает получение и поиск сообщений с использованием сетевого уровня.

3. Контроллер просмотра

Он имеет экземпляр модели представления и соответствует протоколу ViewModelDelegate . Он вызывает функции модели представления и получает ответ через обратные вызовы делегата.

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

Прежде чем мы начнем писать наши тесты, давайте завершим наш URLSessionProxy, как показано ниже:

Не закрывайте статью 🤣. Позвольте мне разобрать, что происходит:

startLoading()

Этот метод вызывается системой после того, как мы соглашаемся обработать запрос URL, возвращая true внутри func canInit(with request: URLRequest) -> Bool. Мы должны начать обработку запроса и сообщить клиенту, когда это будет сделано.

Обработчик запроса

Мы будем использовать этот объект модели для установки response, data или error для запросов.

Статические свойства URLSessionProxy

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

  1. contactedURLs: [URL]

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

2. shouldHandleRequest: ((URLRequest) -> Bool) = { _ in true }

Вы можете использовать это закрытие, чтобы решить, хотите ли вы обрабатывать конкретный запрос. Это вызывается внутри func canInit(with request: URLRequest) -> Bool.

3. handleRequest: ((URLRequest) -> RequestHandler)?

Это вызывается внутри func startLoading(). Вы можете использовать это закрытие, чтобы решить, как вы хотите реагировать на URLRequest , возвращая экземпляр RequestHandler.

Теперь все готово! Приступаем к написанию тестов.

Тест 1: вызов API пользовательского фида запускается на viewDidLoad()

Ожидание: URLSessionProxy массив contactedURLs содержит URL-адрес API пользовательского фида.

Мы настраиваем наш URLSessionProxy так, чтобы он возвращал экземпляр RequestHandler instance по умолчанию, поскольку мы не сосредоточены на этой части в этом тесте. Затем мы вызываем viewDidLoad() для модели view , чтобы запустить процесс.

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

Тест 2: пользователи могут видеть сообщения в своей ленте

Ожидание: при вызове viewDidLoad() модель представления содержит фиктивные сообщения после вызова reloadPosts() для своего делегата.

Настраиваем следующее:

  1. Создайте делегата модели view и выполните ожидание вызова метода делегата, чтобы мы знали, что пришло время проверить модель представления на наличие фиктивных сообщений [String]
  2. Настройте URLSessionProxy для возврата фиктивного ответа
  3. Call viewDidLoad() в модели представления, чтобы запустить процесс

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

Тест 3: Поиск сообщений работает правильно для пользователей

Ожидание: API поиска вызывается с правильными параметрами.

Мы настраиваем наш URLSessionProxy на захват результатов поиска URLRequest и сохраняем их в нашей локальной переменной.

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

Затем мы исследуем различные части нашего URLRequest , чтобы проверить, все ли параметры соответствуют ожидаемым.

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

Готовый проект доступен здесь. Не стесняйтесь исследовать! Также существует библиотека с открытым исходным кодом, основанная на этой методике.