Отличный набор тестов для iOS - быстрый, надежный, точный и воспроизводимый. Распространенной проблемой, которая делает автоматическое тестирование в iOS медленным и нестабильным, является наличие неожиданных побочных эффектов и артефактов во время выполнения модульных тестов.
Например, неожиданное состояние / артефакты сделают:
- Некоторые тесты терпят неудачу после запуска приложения
- Некоторые тесты терпят неудачу после незавершенного пробного запуска
- Даже если вы очистите состояние в
tearDown
, если каким-то образом набор тестов не завершится должным образом (например, точка останова или остановлен вручную),tearDown
может никогда не быть выполнен (поэтому состояние никогда не очищается) - Некоторые тесты терпят неудачу из-за временной связи при запуске:
- в случайном порядке
- в изоляции
- весь набор тестов
Приведенные выше примеры сделают ваш набор тестов iOS медленным, ненадежным, неточным и невоспроизводимым. Это одна из причин, по которым выполнение реальных сетевых запросов и / или сохранение состояния на диске во время модульных тестов нежелательно.
Чтобы ваш набор тестов iOS был быстрым, надежным, точным и воспроизводимым, каждый запуск теста должен начинаться в чистом состоянии и заканчиваться в чистом состоянии (без побочных эффектов / артефактов).
Для решения проблемы разработчики iOS справедливо заменят сетевые клиенты и базы данных на какой-то тестовый дублер. Однако это может не решить проблему полностью.
Вот общий вопрос, который мы получаем:
«Я имитирую свой HTTP APIClient, но при выполнении тестов я все еще могу видеть в журналах запускаемые сетевые запросы. Это замедляет мои тесты, и иногда я получаю неожиданные ошибки.
Например, если я запускаю свое приложение и вхожу в систему (например, вручную или в тесте пользовательского интерфейса), я получаю кучу сбоев и сетевых запросов в моей цели модульного теста, потому что состояние пользователя, вошедшего в систему, сохраняется, и мы запускаем куча запросов в AppDelegate на обновление данных вошедшего в систему пользователя.
Как это может случиться, если я имитирую свой HTTP APIClient в тестах?
Я могу чистить симулятор перед каждым запуском, но это также сильно замедляет процесс тестирования (сброс занимает несколько секунд или даже минут).
Что я делаю не так и как решить эту проблему? »
Причина, по которой реальные сетевые запросы будут запускаться при запуске набора тестов (даже если HTTP APIClient
имитируется), заключается в том, что, вероятно, к тестовой цели подключено хост-приложение.
Если у вашей тестовой цели есть хост-приложение, каждый раз, когда вы запускаете свои тесты, приложение также будет запускаться с ними. Например, если вы посмотрите на симулятор во время выполнения тестов, вы увидите, что начальный интерфейс визуализируется в зависимости от состояния приложения.
Таким образом, создается экземпляр AppDelegate
, и он запускает запросы API через реальный APIClient
, как если бы вы просто запустили приложение. Такое поведение может быть желательным для целевого объекта сквозного теста пользовательского интерфейса, но не для целевого объекта изолированного / модульного теста.
Если это ваш случай, вместо того, чтобы настраивать параметры Xcode для очистки состояния запуска или обвинять архитектуру вашего приложения, рассмотрите следующие решения (в порядке наших предпочтений):
- Настройте тесты для запуска без хост-приложения.
- Создайте новую тестовую цель для независимых от приложений тестов без хост-приложения.
- Измените точку входа приложения, создав файл
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 и получите более подробный подобный контент.