Пользователь
0,0
рейтинг
19 февраля 2014 в 04:05

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

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


Введение


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

Однако как показал беглый осмотр пары десятков популярных библиотек на 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.


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

Ссылки


Ян @Yan169
карма
50,5
рейтинг 0,0

Похожие публикации

Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (3)

  • 0
    Зато xctool умеет выдавать отчет о тестах в различных форматах, в том числе JUnit
  • +1
    А еще к xcodebuild можно добавить XCPretty, очень здоровский форматтер билд логов, который появился совсем недавно.
    • 0
      Спасибо. Хороший проект, а я его как-то пропустил.
      Для интеграции с подходом из статьи потребуется внести пару изменений в travis.yml и скрипты. В частности, поскольку xcpretty используется через pipe, то для проверки успешности прогона тестов вместо кода возврата надо проверять переменную PIPESTATUS:
      xcodebuild test -project $project "$@" | xcpretty -c
      local exitcode=${PIPESTATUS[0]}
      

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.