- PVSM.RU - https://www.pvsm.ru -
В этой статье я хочу рассказать о тестировании iOS приложений и небольшой автоматизации этого процесса.
Под катом будут рассмотрены инструменты для модульного и функционального тестирования и приведены простые примеры.
В качестве примера я решил написать простейший калькулятор. Это приложение не имеет никакой практической пользы и имеет весьма скудный функционал, но на мой взгляд его вполне достаточно для того чтобы дать старт написанию тестов.
Для удобства я выложил [1] готовое приложение на гитхаб.
Для написания модульных тестов я использую замечательный инструмент — Cedar [2].
Он позволяет писать тесты в стиле RSpec [3], что улучшает структурированность и читабельность кода.
На гитхабе дано достаточно полное описание того как собрать фрэймворк, но когда встал вопрос сборки на нескольких машинах, был написан простой скрипт [4] на bash'е, который выполнял всю эту рутину.
Для установки необходимо склонировать проект и запустить скрипт install.sh, Cedar будет установлен в /opt/cedar и будут добавлены символические ссылки в домашнюю директорию пользователя (для большего удобства при подключении в проекты).
После того как Cedar собран нужно настроить тестовый таргет.
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'е есть несколько главных «сущностей»:
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
Теперь все должно пройти хорошо.
Такой способ не очень удобен, но он вполне оправдан, т.к. 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
Нажмите здесь для печати.