Xcode и Travis: запуск тестов на множестве конфигураций

от автора

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

Введение

Любите ли вы баги? Я вот некоторые обожаю: толстые сочные фатальные баги — сплошное удовольствие, их невозможно не заметить и выпустить с ними релиз. Однако бывают и маловероятные маленькие багики, которых не видно невооруженным глазом, но которые норовят себя проявить в каких-нибудь специфических условиях, например, только на определенной версии ОС, или исключительно при включенной оптимизации компилятора. Эти маленькие негодяи, будучи непойманными, способны выпить немало крови разработчика и испортить сон по ночам. Парадоксально, но чем реже проявляется баг, тем больше сил порой уходит на его поимку и устранение. Если вы понимаете, что я имею в виду, то вероятно мысль о том, что тесты стоит запускать на всём множестве потенциально используемых сочетаний версий ОС, устройств, и настроек компиляции, вам покажется очевидной.

Однако как показал беглый осмотр пары десятков популярных библиотек на GitHub, большинство разработчиков, даже используя автоматический запуск тестов для каждого коммита (через Travis-CI), всё равно тестируют только одну конфигурацию для последней версии iOS. Единицы в дополнение прогоняют тесты на OS X, но на этом дело и ограничивается. Посмотрим, как можно сделать по-другому.

В принципе, встроенный в XCode 5 модуль Continuous Integration позволяет запускать тесты на нескольких симуляторах, но не предоставляет достаточной гибкости, и вообще многим пришелся не по душе. Поэтому в этой статье его подробно рассматривать не будем, к тому же о нём уже писали.

xcodebuild

Для тестирования мы будем использовать утилиту xcodebuild, которая идёт в составе Xcode Command Line Tools. Ранее большинство предпочитало xctool от Facebook, т.к. во-первых xcodebuild не заводился без бубнов и плясок, а во-вторых xctool выдаёт более приятный на глаз вывод. Однако с выходом Xcode 5 ситуация поменялась в пользу родного xcodebuild: бубны стали не нужны, а xctool на данный момент не позволяет задать тип симулятора, собственно это ключевой для нас момент.

Типичная команда тестирования выглядит так:

xcodebuild test -project {project}.xcodeproj -scheme {scheme} -sdk iphonesimulator -destination OS=6.0,name=iPhone -configuration Release 

Всё достаточно очевидно: указываем проект (или workspace), схему, SDK, версию ОС, название симулятора и конфигурацию Debug/Release.

Небольшим скриптом мы легко можем перебрать и протестировать все возможные конфигурации (предполагая, что Deployment Target у нас 5.0, и тестируем мы универсальную библиотеку для iOS и OS X):

for configuration in Release Debug do     for device in "iPhone" "iPad"     do         for iosversion in 6.0 6.1 5.0 5.1         do             test_ios iOSTests "$iosversion" "$device" "$configuration"         done     done     for device in "iPhone Retina (3.5-inch)" "iPhone Retina (4-inch)" "iPad Retina"     do         for iosversion in 6.0 6.1 7.0         do             test_ios iOSTests "$iosversion" "$device" "$configuration"         done     done     for device in "iPhone Retina (4-inch 64-bit)" "iPad Retina (64-bit)"     do         test_ios iOSTests-64bit 7.0 "$device" "$configuration"     done     test_osx OSXTests "$configuration" done 

(test_ios и test_osx — функции для тестирования, полная версия скрипта доступна в тестовом проекте на GitHub)

Это даст нам 40 возможных конфигураций, что пожалуй выглядит уже избыточным. Ведь тестировать логику на разных разрешениях или UI при разных настройках оптимизации особого смысла нет. А если вы успели обновить систему до OS X Mavericks, то успели заметить, что на ней не работают симуляторы iOS 5.

Итого, в дальнейшем в статье мы будем тестировать следующие конфигурации:

Logic Tests iOS 6.0 iOS 6.1 iOS 7.0 iOS 7.0 64-bit OS X
Release
Debug
UI Tests iPhone iPad iPhone Retina (3.5-inch) iPhone Retina (4-inch) iPad Retina
iOS 6.0
iOS 7.0

Для реальных проектов набор актуальных конфигураций будет для каждого свой, конечно.

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

test-main-configurations.sh

#!/bin/sh  # Global settings project=XCode/TravisCI.xcodeproj 

Formatting output

function red() {     eval "$1=\"$(tput setaf 1)$2$(tput sgr 0)\"" } function green() {     eval "$1=\"$(tput setaf 2)$2$(tput sgr 0)\"" } function yellow() {     eval "$1=\"$(tput setaf 3)$2$(tput sgr 0)\"" } function bold() {     eval "$1=\"$(tput bold)$2$(tput sgr 0)\"" } function echo_fmt() {     local str=$1     local color=$2     local bold=$3     if [ "$color" != '' ]; then          $color str "$str"      fi     if [ "$bold" != '' ]; then          $bold str "$str"      fi     echo $str } 

Testing

succeeded_count=0 function test() {     local options="$@"     echo_fmt "xcodebuild test -project $project $options" yellow      xcodebuild test -project $project "$@"     local exitcode=$?     if [[ $exitcode != 0 ]] ; then         echo_fmt "xcodebuild exited with code $exitcode" red         echo_fmt "=== TESTS FAILED ===" red bold         exit 1     else         ((succeeded_count++))     fi }  function test_ios() {     local scheme=$1     local iosversion=$2     local device="$3"     local configuration=$4     shift 4     echo_fmt "=== TEST SCHEME $scheme IOS $iosversion DEVICE $device CONFIGURATION $configuration ===" yellow bold      test -scheme "$scheme" \          -sdk iphonesimulator \          -destination OS="$iosversion",name="$device" \          -configuration "$configuration" \          "$@" }  function test_osx() {     local scheme=$1     local configuration=$2     shift 2     echo_fmt "=== TEST SCHEME $scheme OSX CONFIGURATION $configuration ===" yellow bold     test -scheme "$scheme" -configuration "$configuration" "$@" } 

# Logic tests for configuration in Release Debug do     for iosversion in 6.0 6.1 7.0 #5.0 5.1 # Mavericks does not support iOS 5 Simulator     do         test_ios "iOSLogicTests" "$iosversion" "iPad Retina" "$configuration"     done     test_ios "iOSLogicTests-64bit" 7.0 "iPad Retina (64-bit)" "$configuration" ONLY_ACTIVE_ARCH=YES     test_osx "OSXTests" "$configuration"     done  # UI tests test_ios "iOSUITests" 6.0 "iPhone" Debug for device in "iPad" "iPhone Retina (3.5-inch)" "iPhone Retina (4-inch)" "iPad Retina" do     for iosversion in 6.0 7.0     do         test_ios "iOSUITests" "$iosversion" "$device" Debug     done done  # Result echo_fmt "=== SUCCEEDED $succeeded_count CONFIGURATIONS. ===" green bold 

Запуск которого:

./Script/test-main-configurations.sh 

выдаст нам либо сообщение об удачно пройденных тестах:

=== SUCCEEDED 19 CONFIGURATIONS ===

либо сообщение об ошибке:

=== TESTS FAILED ===

О тестировании на симуляторе iOS 64-bit

Из-за какого-то бага Xcode часто не даёт запускать тесты на 64-битном симуляторе и ругается странным сообщением:

Исправляется это безобразие установкой «Build Active Architecture Only»=«YES» для тестируемой конфигурации (Debug/Release). Поэтому в скрипте для 64-битных тестов присутствует дополнительная опция ONLY_ACTIVE_ARCH=YES.

XCode

Запускать скрипт вручную кому-то может прийтись не по душе, поэтому можно создать отдельный таргет, который будет его запускать. Для этого в XCode делаем манипуляции Add Target… -> Other -> Aggregate, затем Editor -> Add Build Phase -> Add Run Script Build Phase, и добавляем следующий скрипт:

cd ${SRCROOT}/.. # переходим в корень репозитория scriptname="test-main-configurations" script="Script/${scriptname}.sh" log="Script/${scriptname}.log" $script > $log # запускаем скрипт, вывод записываем в файл if [[ $? != 0 ]] ; then # если скрипт завершился неудачей, ...     echo "error: TESTS FAILED" # ... то показываем ошибку и выходим, ...     exit 1 else     rm $log # ... иначе удаляем временный файл fi 


Теперь если сборка этого таргета (+B) успешно завершилась, значит тесты пройдены успешно, если нет — идём в лог Scripts/test-main-configurations.log и смотрим, в чём причина.
Конечно, запускать вручную такое тестирование перед каждым коммитом никакой силы воли не хватит, но вполне можно сделать это обязательным этапом при выпуске нового релиза, например добавив аналогичный Build Phase в ветке release DVСS.

Travis-CI

Но конечно, эффективнее, когда тесты регулярно прогоняются автоматически, для этого настроим систему интеграции Travis-CI, которая будет при каждом push на GitHub автоматически прогонять все наши тесты. Излишне подробно я описывать не буду, благо уже была соответствующая статья, остановлюсь только на необходимых моментах.

Всё, что нам понадобится, это добавить в корень репозитория файлик .travis.yml, в котором будет указано, что надо запускать наш скрипт:

language: objective-c script: Script/test-main-configurations.sh 

Ну и авторизоваться на Travis-CI и включить галочку для нужного репозитория. В принципе, всё. Теперь коммиты будут тщательно протестированы, а pull-запросы сопровождаться сообщениями от Travis:

Ещё не забудьте проверить, что тестовые схемы присутствуют в репозитории, т.е. для них установлен флажок Shared:

Кстати, Travis использует Xcode 5.0.2 на OS X 10.8.5, поэтому может прогонять тесты на iOS 5.0 и iOS 5.1.

Тестирование на устройстве.

Если у вас сохранилось устройство на iOS 5, то тесты можно запускать и на нём, написав аналогичный скрипт для нескольких конфигураций. Логические тесты запускаются только на симуляторе, а на устройстве можно запускать только тесты приложений. Поэтому, если вы тестируете библиотеку, то придется создать пустое приложение-контейнер с вашей библиотекой, и тестировать его.
Команда xcodebuild для тестирования на устройстве выглядит примерно так:

xcodebuild test -project XCode/TravisCI.xcodeproj -scheme iOSDeviceLogicTests -sdk iphoneos -destination name='iPad Yan' -configuration Release 

Если что-то не завелось, то смотрите спойлер

  • Возможны проблемы с provisioning profile. Проверьте, запускается ли приложение на устройстве из XCode.
  • Проверьте, что для таргета с тестами в Build Phases в Target Dependencies присутствует приложение-контейнер.
  • Параметр Bundle Loader в Build Settings должен быть установлен в:
    $(BUILT_PRODUCTS_DIR)/MyExistingApp.app/MyExistingApp 
  • А параметр Test Host иметь значение:
    $(BUNDLE_LOADER) 
  • Кроме того, в настройках сборки приложения-контейнера параметр Symbols Hidden by Default должен быть равен NO.

На этом всё. Удачного тестирования!

Ссылки

ссылка на оригинал статьи http://habrahabr.ru/post/212615/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *