- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

На гитхабе дано достаточно полное описание того как собрать фрэймворк, но когда встал вопрос сборки на нескольких машинах, был написан простой скрипт [4] на 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 [5] и Frank [6].

Эти инструменты очень похожи, они оба позволяют писать тесты на Cucumber'е [7] и оба реализованы на 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'ов [8] довольно обширный, но нам нужен guard-calabash-ios [9].

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

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

Это создаст Guardfile — файл в котором описаны свойства необходимые guard'у и файлы за которыми нужно следить. (Детальные настройки можно найти на гитхабе [10].)
Последний штрих — откройте настройки 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, надеюсь что это у меня вышло и для статья окажется полезной.

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

Автор: 1101_debian


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ios/8382

Ссылки в тексте:

[1] выложил: https://github.com/AlexDenisov/tdd_calc

[2] Cedar: https://github.com/pivotal/cedar

[3] RSpec: http://rspec.info/

[4] скрипт: https://github.com/AlexDenisov/Cedar-installer

[5] Calabash-iOS: https://github.com/calabash/calabash-ios

[6] Frank: https://github.com/moredip/Frank

[7] Cucumber'е: http://cukes.info/

[8] Список guard'ов: https://github.com/guard/guard/wiki/List-of-available-Guards

[9] guard-calabash-ios: https://github.com/AlexDenisov/guard-calabash-ios

[10] гитхабе: https://github.com/AlexDenisov/guard-calabash-ios#readme

[11] Guard Frank: https://github.com/AlexDenisov/guard-frank