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

Например, неожиданное состояние / артефакты сделают:

  • Некоторые тесты терпят неудачу после запуска приложения
  • Некоторые тесты терпят неудачу после незавершенного пробного запуска
  • Даже если вы очистите состояние в tearDown, если каким-то образом набор тестов не завершится должным образом (например, точка останова или остановлен вручную), tearDown может никогда не быть выполнен (поэтому состояние никогда не очищается)
  • Некоторые тесты терпят неудачу из-за временной связи при запуске:
  • в случайном порядке
  • в изоляции
  • весь набор тестов

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

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

Для решения проблемы разработчики iOS справедливо заменят сетевые клиенты и базы данных на какой-то тестовый дублер. Однако это может не решить проблему полностью.

Вот общий вопрос, который мы получаем:

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

Например, если я запускаю свое приложение и вхожу в систему (например, вручную или в тесте пользовательского интерфейса), я получаю кучу сбоев и сетевых запросов в моей цели модульного теста, потому что состояние пользователя, вошедшего в систему, сохраняется, и мы запускаем куча запросов в AppDelegate на обновление данных вошедшего в систему пользователя.

Как это может случиться, если я имитирую свой HTTP APIClient в тестах?

Я могу чистить симулятор перед каждым запуском, но это также сильно замедляет процесс тестирования (сброс занимает несколько секунд или даже минут).

Что я делаю не так и как решить эту проблему? »

Причина, по которой реальные сетевые запросы будут запускаться при запуске набора тестов (даже если HTTP APIClient имитируется), заключается в том, что, вероятно, к тестовой цели подключено хост-приложение.

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

Таким образом, создается экземпляр AppDelegate, и он запускает запросы API через реальный APIClient, как если бы вы просто запустили приложение. Такое поведение может быть желательным для целевого объекта сквозного теста пользовательского интерфейса, но не для целевого объекта изолированного / модульного теста.

Если это ваш случай, вместо того, чтобы настраивать параметры Xcode для очистки состояния запуска или обвинять архитектуру вашего приложения, рассмотрите следующие решения (в порядке наших предпочтений):

  1. Настройте тесты для запуска без хост-приложения.
  2. Создайте новую тестовую цель для независимых от приложений тестов без хост-приложения.
  3. Измените точку входа приложения, создав файл main.swift без создания экземпляра делегата приложения.

1. Запустите тесты без хост-приложения.

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

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

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

В Xcode на вкладке «Общие» тестовой цели вы можете изменить параметр Host Application на «none». При этом тесты будут выполняться без запущенного приложения.

Удаление хост-приложения также ускоряет загрузку тестов, и нет необходимости вносить какие-либо изменения в производственную цель (например, проверять аргументы запуска «IS_TESTING»).

Однако, если компоненты, которые вы хотите протестировать, находятся в целевом объекте приложения, у вас не будет доступа к ним в тестовом целевом объекте:

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

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

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

2. Создайте новую тестовую цель без хост-приложения.

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

В Xcode создайте новую цель тестирования и переместите все тесты, независимые от приложения, в более быструю и надежную цель изолированного / модульного тестирования без хост-приложения. Таким образом, приложение не будет загружаться, если вы запустите эти тесты независимо.

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

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

3. Замените свой класс AppDelegate при запуске тестов.

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

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

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

Исторически в Objective-C основной точкой входа для приложения iOS была основная функция, определенная в файле main.m, где вы должны были создать экземпляр UIApplication с классом AppDelegate.

Однако в Swift этот код «автоматически сгенерирован» для вас при использовании атрибута @UIApplicationMain над объявлением класса AppDelegate.

Атрибут @UIApplicationMain означает, что AppDelegate является делегатом приложения. Когда вы запускаете свои тесты с хост-приложением, вы также запускаете свое приложение со значением по умолчанию AppDelegate, которое может выполнять сетевые запросы, события аналитики, побочные эффекты базы данных и много другой ненужной работы.

Чтобы обойти основную точку входа по умолчанию, вы можете удалить атрибут @UIApplicationMain из своего AppDelegate подкласса и создать новый файл с именем main.swift на верхнем уровне вашего проекта.

В пользовательском main.swift, аналогичном старому Objective-C main.m, вы должны явно вызвать функцию UIApplicationMain, передав класс делегата:

Следующий фрагмент кода ниже показывает, что требуется добавить в main.swift file к:

  • Запустите UIApplication без UIApplicationDelegate при тестировании
  • Начните UIApplication с UIApplicationDelegate в производстве

Обратите внимание, что функция func delegateClassName() проверяет, может ли среда выполнения Swift найти класс XCTestCase. XCTestCase доступен только при выполнении тестов, поэтому его нельзя загрузить в производственной среде.

Возвращаемое значение delegateClassName() используется в качестве аргумента имени класса делегата в функции UIApplicationMain, которая является основной точкой входа в приложение.

Если среда выполнения Swift не может найти класс XCTestCase, будет возвращено имя вашего AppDelegate подкласса, в противном случае - nil.

Таким образом, если вы запускаете тесты, Приложение не будет создавать реальный экземпляр AppDelegate, поскольку значение имени класса делегата - nil.

В качестве альтернативы вы можете создать собственный UIApplicationDelegate в своей тестовой цели и использовать его вместо того, чтобы возвращать nil имя класса делегата:

Решения, которых следует избегать

Есть и другие популярные решения, такие как добавление аргументов запуска или условий времени компиляции, таких как #if TESTING, но, по нашему опыту, они обычно приносят больше вреда, чем помогают.

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

Резюме

Чтобы создать быстрый, надежный, точный и воспроизводимый набор тестов iOS, важно исключить неожиданные / ненужные побочные эффекты и артефакты во время выполнения модульных тестов. Запуск модульных тестов с хост-приложением - частый источник таких проблем.

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

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

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

Первоначально опубликовано на https://www.essentialdeveloper.com.

Давайте подключимся

Если вам понравилась эта статья, посетите нас по адресу https://essentialdeveloper.com и получите более подробный подобный контент.

Следуйте за нами в: YouTubeTwitterFacebookGitHub