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

в 7:20, , рубрики: bdd, cedar, cucumber, guard, iOS, objective-c, rspec, tdd, ui testing, Песочница, разработка под 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, надеюсь что это у меня вышло и для статья окажется полезной.

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

Автор: 1101_debian


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js