Основную идею данной заметки в принципе можно уместить в одном предложении: «раз уж вы пишете тесты, то неплохо было бы прогонять их на всех возможных конфигурациях, а не на одной единственной». Но поскольку формат однострочных статей на хабре не принят, а информация усваивается пропорционально логарифму количества слов в объяснении, то раскрою мысль подробнее.
Введение
Любите ли вы баги? Я вот некоторые обожаю: толстые сочные фатальные баги — сплошное удовольствие, их невозможно не заметить и выпустить с ними релиз. Однако бывают и маловероятные маленькие багики, которых не видно невооруженным глазом, но которые норовят себя проявить в каких-нибудь специфических условиях, например, только на определенной версии ОС, или исключительно при включенной оптимизации компилятора. Эти маленькие негодяи, будучи непойманными, способны выпить немало крови разработчика и испортить сон по ночам. Парадоксально, но чем реже проявляется баг, тем больше сил порой уходит на его поимку и устранение. Если вы понимаете, что я имею в виду, то вероятно мысль о том, что тесты стоит запускать на всём множестве потенциально используемых сочетаний версий ОС, устройств, и настроек компиляции, вам покажется очевидной.
Однако как показал беглый осмотр пары десятков популярных библиотек на 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 | ✓ | ✓ | ✓ | ✓ |
Для реальных проектов набор актуальных конфигураций будет для каждого свой, конечно.
Добавив обвязку для подсчета успешно протестированных конфигураций и выхода при первой неудаче, получим
финальную версию скрипта:
#!/bin/sh
# Global settings
project=XCode/TravisCI.xcodeproj
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
}
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 ===
Исправляется это безобразие установкой «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.
На этом всё. Удачного тестирования!
Ссылки
Автор: Yan169