Тестирование iOS-приложений

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

Тестовое приложение


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

Для удобства я выложил готовое приложение на гитхаб.

Модульные тесты


Для написания модульных тестов я использую замечательный инструмент — Cedar.
Он позволяет писать тесты в стиле RSpec, что улучшает структурированность и читабельность кода.

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

Для установки необходимо склонировать проект и запустить скрипт install.sh, Cedar будет установлен в /opt/cedar и будут добавлены символические ссылки в домашнюю директорию пользователя (для большего удобства при подключении в проекты).

После того как Cedar собран нужно настроить тестовый таргет.
  1. Добавьте к вашему проекту новый таргет (Empty Application), я назвал его UnitTests.
  2. Прилинкуйте Cedar к таргету (Link Binary With Libraries)
  3. Добавьте в Other Linker Flags -ObjC -all_load -lstdc++
  4. Удалите AppDelegate. Этого можно и не делать, но он нам не нужен.
  5. Отредактируйте main.m как показано ниже

int main(int argc, char *argv[])
{
    @­autoreleasepool{
        return UIApplicationMain(argc, argv, nil, @"CedarApplicationDelegate");
    }
}

Давайте напишем первый тест.
Создайте в UnitTests target файл FooBar.mm с таким содержанием:

#import <Cedar-iOS/SpecHelper.h>
using namespace Cedar::Matchers;

SPEC_BEGIN(FooSpec)

beforeEach(^{
    NSLog(@"before each");
});
afterEach(^{
    NSLog(@"after each");
});

describe(@"Foo", ^{
    it(@"YES should be YES", ^{
        YES should equal(YES);
    });
});

SPEC_END


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

Вернемся к калькулятору. Допустим у нас будет какой-нибудь класс-синглтон, который будет заниматься вычислениями, назовем его CalculationManager. У него должен быть метод который должен возвращать инстанс этого класса, назовем его sharedInstance.
Напишем тест на этот кейс.
Создайте пустой класс CalculationManager в вашем главном target'е и добавьте еще один файл для тестов (к примеру CalculationManager.mm) с таким содержимым:

//    CalculationManagerSpecs.mm

#import <Cedar-iOS/SpecHelper.h>
using namespace Cedar::Matchers;
#import "CalculationManager.h"

SPEC_BEGIN(CalculationManagerSpecs)

describe(@"CalculationManager", ^{
    it(@"sharedInstance should not return nil", ^{
        id instance = [CalculationManager sharedInstance];
        instance should_not be_nil;
    });
});

SPEC_END


При запуске видно что тест валится.
Добавьте реализацию чтоб тест проходил успешно и продолжим.

Добавим пару тестов на операции сложения и вычитания.

describe(@"calculations should be correct", ^{
CalculationManager *manager = [CalculationManager sharedInstance];
    it(@"addition should be correct", ^{
        NSInteger left = 5;
        NSInteger right = 37;
        NSInteger etalonResult = 42;
        NSInteger realResult = [manager add:left with:right];
        realResult should equal(etalonResult);
    });
    it(@"subtract should be correct", ^{
        NSInteger left = 14;
        NSInteger right = 12;
        NSInteger etalonResult = 2;
        NSInteger realResult = [manager subtract:right from:left];
        realResult should equal(etalonResult);
    });
});


На этом все, будем считать что прилжение достаточно покрыто модульными тестами, и перейдем к тестированию интерфейса.

Тестирование интерфейса


Существует немало средств для тестирования интерйеса iOS приложений, но я хочу рассказать о тех которыми пользуюсь сам, а именно Calabash-iOS и Frank.

Эти инструменты очень похожи, они оба позволяют писать тесты на Cucumber'е и оба реализованы на Ruby, разница лишь в функционале.
В одном из проектов мне пришлось мигрировать с Frank'а, я просто запустил тесты с использованием Calabash'а и все они прошли почти сразу, пришлось только немного изменить пару шагов.

Сейчас я остановился на Calabash. Думаю что многие iOS разработчики не знакомы с Cucumber, потому и хочу немного рассказать как он работает и как писать тесты.

Cucumber

Я ни в коем случае не претендую на верность этого описания, просто опишу как я понимаю его работу, и надеюсь это описание внесет некоторую ясность и поможет стартануть тем кто все еще не решился его использовать.

Итак, в Cucumber'е есть несколько главных «сущностей»:

Feature — это набор нескольких связанных по логике сценариев (или не связанных, уж как программист решит). Она состоит из названия и краткого, информативного описания. К примеру:

Feature: Manage Orders
    As a User I should be able to manage Orders through iOS application

Scenario — конкретный сценарий описывающий некоторый use case. Состоит из имени и набора шагов.

Scenario: Create Order
    #steps

Step — описание конкретного действия пользователя (нажатие на кнопку/ссылку, ввод текста, свайп и.т.п).

When I fill in "Title" with "FuuBar"
And I touch "Save" button
Then I should see alert view titled "Saved successully"

Step definition — реализация конкретного действия пользователя. Выглядит она примерно так:

When /^I touch "([^"]*)" button$/ do |button_text|
    touch("button marked:#{button_text}")
end


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

Давайте добавим Calabash в наш проект.
Перейдите в деректорию с проектом и выполните следующие команды:

[sudo] gem install calabash-cucumber
calabash-ios setup
calabash-ios gen

Calabash добавил к нашему проекту еще один таргет, по умолчанию он имеет шаблон project_name-cal. Нам необходимо выполнить build для этой цели.
Теперь мы почти готовы запустить тесты.
После генерации выводится подсказка как запустить тесты

DEVICE=iphone OS=ios5 cucumber


но на выполнении этой команды все валится, т.к. calabash не знает где находится наше приложение. Для этого необходимо указать еще одну переменную — APP_BUNDLE_PATH. По умолчанию Xcode 4.x хранит приложения по адресу

~/Library/Application\ Support/iPhone\ Simulator/x.x/Applications/hash/app_name.app

где x.x — версия iOS, а hash — сгенерированный Xcode'ом уникальный ключ для приложения.
Попробуйте найти свой .app и выполнить следующее


APP_BUNDLE_PATH='~/Library/Application\ Support/iPhone\ Simulator/x.x/Applications/hash/your_app-cal.app' DEVICE=iphone OS=ios5 cucumber

Теперь все должно пройти хорошо.

Guard

Такой способ не очень удобен, но он вполне оправдан, т.к. calabash не может знать где лежит наше приложение. И тут нам на помощь приходит Guard.
Guard — это гем, который следит за файловой системой и при изменении файлов, за которыми он следит выполняет какие-либо операции. Список guard'ов довольно обширный, но нам нужен guard-calabash-ios.

Для его установки и использования необходимо выполнить следующее:

gem install guard-calabash-ios
guard init calabash-ios

Это создаст Guardfile — файл в котором описаны свойства необходимые guard'у и файлы за которыми нужно следить. (Детальные настройки можно найти на гитхабе.)
Последний штрих — откройте настройки Xcode и установите Derived Data как Relative. Теперь Xcode будет хранить сборки в директории с проектом, что позволит скрипту из guard-calabash-ios найти нужный нам APP_BUNDLE_PATH автоматически.
Теперь для прогона тестов необходимо выполнить в папке с проектом следующее.

guard


Пишем тесты

Теперь когда все работает более удобно мы можем приступить к написанию наших UI-тестов.

Calabash создал папку features, в которой находятся наши сценарии и реализация шагов.
Давайте удостоверимся что наш калькулятор позволит пользователю сложить или вычесть два числа, и показать верный результат в alert view.

Отредактируйте файл my_first.feature

Feature: Add numbers
  As a User I should be able to perform calculations

  Scenario: Add numbers
    When I fill in "left" with "15"
    And I fill in "right" with "10"
    And I touch "add"
    Then I should see "25"

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

Итак, все тесты провалились, что логично.

Давайте добавим UI.

Для доступа к контролам из calabash'а нужно задать им accessibility label. Кроме того к кнопкам можно обращаться по надписи на них, а к текстовым полям по плэйсхолдеру.

Я сделал примитивный интерфейс: два текстовых поля и две кнопки в navigation bar'е, "+" и "-".
После того как мы добавили контролы на наш экран нам нужно выполнить следующие действия:

1. Добавить outlet'ы для кнопок и текстовых полей
2. Задать плэйсхолдеры нашим текстовым полям «left» и «right»
3. Задать accessibility label'ы для кнопок

self.addButton.accessibilityLabel = @"add";
self.subtractButton.accessibilityLabel = @"subtract";

4. Повесить обработчики на наши кнопки

- (IBAction)addButtonTapped:(id)sender {
    CalculationManager *calculationManager = [CalculationManager sharedInstance];
    NSInteger left = [self.leftTextField.text integerValue];
    NSInteger right = [self.rightTextField.text integerValue];
    [self showResult:[calculationManager add:left with:right]];
}

- (IBAction)subtractButtonTapped:(id)sender {
    CalculationManager *calculationManager = [CalculationManager sharedInstance];
    NSInteger left = [self.leftTextField.text integerValue];
    NSInteger right = [self.rightTextField.text integerValue];
    [self showResult:[calculationManager subtract:right from:left]];
}


5. Добавить метод для отображения результата

- (void)showResult:(NSInteger)result {
    NSString *resultString = [NSString stringWithFormat:@"%d", result];
    [[[[UIAlertView alloc] initWithTitle:@"Result"
		                         message:resultString
		                          delegate:nil
		             cancelButtonTitle:@"OK"
		             otherButtonTitles:nil] autorelease] show];
}


6. Перейдите в терминал с запущенным guard'ом и нажмите Enter, это запустит все ваши сценарии, у нас он один и если вы все сделали правильно, то тесты пройдут успешно.

Теперь напишем тест для вычитания.

Scenario: Subtract numbers
	When I subtract 15 from 38
	Then I should see "23" as result

После запуска Cucumber сообщит что не знает таких шагов, и предложит их реализовать.
Скопируем и немного подредактируем то что он вывел в файл calabash_steps.rb (project_dir/features/steps_definitions/)

When /^I subtract (\d+) from (\d+)$/ do |subtrahend, minuend|
  step %{I fill in "left" with "#{minuend.to_s}"}
  step %{I fill in "right" with "#{subtrahend.to_s}"}
  step %{I touch "subtract"}
end

Then /^I should see "(.*?)" as result$/ do |result|
  res = query("view:'UIAlertView'", "message").first
  res.should == result
end


В реальной жизни мы скорее всего использовали бы теже методы что и в первом сценарии, но здесь я хотел показать как выглядят step definitions, как вызывать другие шаги из реализации шагов(step %{}), как добраться до какого-либо значения (query) и как писать assert (should).

На этом по тестам все.

Заключение


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

В качестве логического завершения еще раз приведу ссылки:

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +1
    Отличная статья!
      +1
      Спасибо, я старался.
      +1
      Спасибо, полезная статья.
        +1
        Интересно, когда я доберусь до использования этих штук…
          +1
          Просто попробуйте, и уже будет не оторвать ;-)
          0
          Спасибо, мне последнее время не хватало подобного руководства.
          +2
          Можете описать почему Вы выбрали не стандартные механизмы тестирования iOS программ из коробки (SenTestKit и Automation)?
          Пример теста:
          <hh user=implementation> iOSControlsTests
          
          - (void)setUp
          {
              [super setUp];
              
              // Set-up code here.
          }
          
          - (void)tearDown
          {
              // Tear-down code here.
              
              [super tearDown];
          }
          
          - (void)testExample
          {
              STFail(@"Unit tests are not implemented yet in iOSControlsTests");
          	NSString* string = nil;
          	STAssertNil(string, @"String should be nil");
          }
          
          <hh user=end>
          
          
            +1
            UIAutomation — когда-то «пробовал».
            Что не понравилось:
            1. Нужно писать тесты на Javascript.
            2. Для запуска тестов нужно открывть какое-то «левое» приложение, что очень неудобно если использовать CI (continuous integration), прийдется писать хитрый скрипт на AppleScript, и я не до конца уверен что это возможно.
            3. И самый главный, который даже не позволил написать простейший тест: приложение должно быть подписано. Подписать приложение не проблема, но зачем я должен делать это для простого обучения?..

            Ну а плюсов я не нашел.
            Стандартный фрэймворк для unit-тестов — пробовал Kiwi на его основе.
            Минусы:
            1. Точки останова в тест-кейсах не работают.
            2. Для тестирования CoreData пришлось писать массу лишнего кода обернутого в #ifdef (хотя может я что-то не так сделал) для того чтобы создать PersistentStorage, часа 4 убил на все это дело.
            3. Опять таки не знаю как это будет работать на CI.

            Плюсы: работает почти «из коробки»
              +1
              К тому же я параллельно пишу на RoR, потому RSpec- и Cucumber-style как-то больше по душе.
              Но это все субъективно, кому что нравится.
                +2
                Ну давайте для начала про SenTestingKit — я оберток на него не использовал, не знаю, что такое Kiwi и какие с ним проблемы, но код из Вашего примера выглядит так:
                <hh user=implementation> CalculationManagerTests
                
                - (void)testSharedInstance
                {
                	STAssertNotNil([CalculationManager sharedInstance], @"sharedInstance should not return nil");
                }
                
                - (void)testMathOperations
                {
                	CalculationManager* manager = [CalculationManager sharedInstance];
                	
                	NSInteger left = 5;
                	NSInteger right = 37;
                	NSInteger etalonResult = 42;
                	NSInteger realResult = [manager add:left with:right];
                	STAssertEquals(etalonResult, realResult, @"addition should be correct");
                	
                	left = 14;
                	right = 12;
                	etalonResult = 2;
                	realResult = [manager subtract:right from:left];
                	
                	STAssertEquals(etalonResult, realResult, @"subtract should be correct");
                }
                <hh user=end>
                


                Плюсы следующие:
                1. Тесты на языке основной программы
                2. Поддержка Xcode — нажимаем cmd + U и видем красивый пабл с инфой прошли тесты или нет
                3. Очень удобно дебажиться, все брейкпоинты работают
                4. Не требует сторонних библиотек и не добавляет ничего в код программы, наоборот в тестовую версию программы ложиться дополнительный бандл

                Хочу заметить, что все это работает как на симуляторе так и на устройстве (я значит сертификат не нужен), проблем с обращением к локальным файлам программы (тот же PersistantStore) нету.

                Про Automation — так же работает как с симулятором так и с девайсом, «родная интеграция» — видит все контролы (полностью повторяет иерархию контролов), в том числе клавиатуру, можно записывать действия для выполнения, а можно писать скрипты.

                Единственное — интеграция с CI действительно не прозрачная, хотя тот же Automation полностью поддерживает запуск из консоли (как часть Instruments).
                  +1
                  Я не пробовал SenTestingKit, но я попробовал использовать Cedar и мне это понравилось. Проблема с CoreData очень сильно оттолкнула от использования стандартных тестов. А писать UI-тесты на Javascript как-то не хочется.

                  Когда я пишу тесты для iOS на Cucumber, то я не учу инструмент_только_для_iOS, а я изучаю универсальный инструмент, он позволяет мне писать теже тесты и для Rails приложений, и если я вдруг буду писать под Android, то мне не придется изучать какой-нибудь специфичный для этой ОС фрэймворк.

                  Говорю ж, все это субъективно, я описал то чем я тестирую, и я ни в окем случае не настаиваю на том чтобы все начали использовать только эти средства :)
                    +1
                    Если Cedar заменить на Kiwi то получим все те же плюшки + более декларативный синтаксис и библотеку для мокинга (не нужно тянуть за собой OCMock). Еще могу сказать, что при написании тестов в RSpec-стиле новичкам гораздо проще понять, что и как надо тестировать.
                      0
                      Брейкпоинты как-то почему-то через раз работали, когда я последний раз мучил SenTestKit.

                      В какой-то презентации про GHUnit (другой движок для тестов) видел аргумент «не нужно иметь PHD, чтобы дебажить тесты» :)

                      А UIAutomation вы в итоге где-нибудь используете? Проблема с UI-тестами, что хорошо бы их писать тестировщику, т.к. девелоперу обычно некогда и лень. Но нужно серьезно уметь программировать, чтобы их написать. Cucumber пытается это исправить, но ИМХО не до конца.

                Only users with full accounts can post comments. Log in, please.