OCUnit в XCode 4.5 для новичков

Однажды мне надоело, что исправление багов занимает у меня больше времени, чем разработка приложения, и в поисках путей решения я пришел к TDD — Test-driven development (Разработка через тестирование).

В это статье рассказывается как делать первые шаги в XCode 4.5, используя unit test-ы, при разработки приложений под IOS.

Статья предназначена для новичков, в ней не содержится информации для зубров разработки.


Введение


Начнем с самого начала, создадим новый проект, отметим галочку «Include Unit Tests» и назовем его Ocu:
file -> new -> project -> single view application
step 1
Я предпочитаю не использовать ARC, так как придерживаюсь правила, что новые фичи должны полежать пока их не доведут до ума, а он появился всего год назад. Да и ручное управление памятью не такая уж сложная штука, и небольшая практика в этом будет полезна.

Мы создали проект в который включен фреймворк SenTestingKit, этот фреймворк и будет нам помогать.
В проекте видим 2 группы — Ocu и OcuTests, в первой содержится код нашего прилложения, во второй юнит тесты.
step 2

Откроем файл «OcuTests.m» и найдем метод «testExample».
OcuTests.m
#import "OcuTests.h"

@implementation OcuTests

- (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 OcuTests");
}

@end

setUp — это метод который выполнится перед началом тестирования, тут можно инициализировать все объекты, которые нам понадобятся для теста.
tearDown — это метод который выполнится после окончания тестирования, тут можно уничтожить все использованные нами объекты.
testExample — это самый первый тест, который мы выполним, он содержит простой макрос который вызовет сообщение об ошибке и напишет, что мы еще не создали ни одного юнит теста.

Вообще, если название метода начинается на test и он возвращает void, то SenTestingKit автоматически распознает его как тест и выполнит.

Запустим тест (нажатием «cmd+U» или нажав на кнопку «Run» и подержав ее, выбираем из выпавшего списка «Test») и посмотрим что нам напишет дебаггер.
Test Suite 'OcuTests' started at 2012-11-22 22:27:01 +0000.
Test Case '-[OcuTests testExample]' started.
/Ocu/OcuTests/OcuTests.m:29: error: -[OcuTests testExample] : Unit tests are not implemented yet in OcuTests
Test Case '-[OcuTests testExample]' failed (0.000 seconds).
Test Suite 'OcuTests' finished at 2012-11-22 22:27:01 +0000.
Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds

Дебаггер написал что выполнен 1 тест, и тест не пройден, и выдал нам описание которое мы поставили в макросе.

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

Пример 1 — проверка сложения


В этом примере мы просто создадим 2 переменные и проверим результат их сложения.

Определим 2 переменные, которые будем складывать, и метод в котором будет содержаться наш тест.
OcuTests.h
#import <SenTestingKit/SenTestingKit.h>

@interface OcuTests : SenTestCase
{
    float foo;
    float bar;
}

- (void) testMathAdd;

@end


В методе setUp проинициализируем переменные и присвоим им значение, а в методе testMathAdd проверим результат сложения.
OcuTests.m
- (void)setUp
{
    [super setUp];
    
    foo = 2.0;
    bar = 5.0;
}

- (void)tearDown
{
    // Tear-down code here.
    
    [super tearDown];
}

- (void) testMathAdd
{
    STAssertTrue (foo + bar == 6.0, @"%f + %f should be 7.0", foo, bar);
}


Запускаем тесты «cmd+U» и видим сообщение об ошибке с комментарием.
Test Suite 'OcuTests' started at 2012-11-23 12:26:14 +0000
Test Case '-[OcuTests testMathAdd]' started.
/Ocu/OcuTests/OcuTests.m:30: error: -[OcuTests testMathAdd] : "foo + bar == 6.0" should be true. 2.000000 + 5.000000 should be 7.0
Test Case '-[OcuTests testMathAdd]' failed (0.000 seconds).
Test Suite 'OcuTests' finished at 2012-11-23 12:20:42 +0000.
Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds


В комментарии указано что результат сложения отличается от ожидаемого, исправим ошибку.
OcuTests.m
- (void) testMathAdd
{
    STAssertTrue (foo + bar == 7.0, @"%f + %f should be 7.0", foo, bar);
}


Запускаем тест и видим сообщение о том, что тест пройден.
Test Suite 'OcuTests' started at 2012-11-23 12:26:14 +0000
Test Case '-[OcuTests testMathAdd]' started.
Test Case '-[OcuTests testMathAdd]' passed (0.000 seconds).
Test Suite 'OcuTests' finished at 2012-11-23 12:26:14 +0000.
Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds


Пример 2 — вычисление значения свойства объекта


В этом примере мы создадим объект, присвоим значения его свойствам и проверим правильно ли создалость еще одно свойство.

Добавим в проект новый класс — выбираем шаблон «Objective-C class» и выбираем «Subclass of NSObject», вводим название «Triangle», при сохранении отмечаем галочкой «Targets OcuTests». В итоге в группе OcuTests получаем 2 новых файла «Triangle.h» и «Triangle.m».
В них создаем необходимые методы.

Triangle.h
#import <Foundation/Foundation.h>

@interface Triangle : NSObject
{
    float cathetus1;
    float cathetus2;
}

- (id)initWithCathetus1:(float)cat1 andCathetus2:(float)cat2;
- (float)getHypotenuse;

@end


Triangle.m
#import "Triangle.h"

@implementation Triangle

- (id)initWithCathetus1:(float)cat1 andCathetus2:(float)cat2
{
    cathetus1 = cat1;
    cathetus2 = cat2;
    
    return self;
}

- (float)getHypotenuse
{
    float hypotenuse = hypotf(cathetus1, cathetus2);
    return hypotenuse;
}

@end


И соответственно делаем изменения в тестах.
OcuTests.h
#import <SenTestingKit/SenTestingKit.h>

@interface OcuTests : SenTestCase

- (void)testTriangleHypotenuse;

@end


OcuTests.m
#import "OcuTests.h"
#import "Triangle.h" // добавляем новый класс, который будем тестировать

@implementation OcuTests

- (void)setUp
{
    [super setUp];
}

- (void)tearDown
{    
    [super tearDown];
}

- (void)testTriangleHypotenuse
{
    float cat1 = 3.0;
    float cat2 = 4.0;
    Triangle *tri = [[Triangle alloc] initWithCathetus1:cat1 andCathetus2:cat2];
    
    STAssertTrue([tri getHypotenuse]==5.0, @"Hypotenuse should be 5.0 with catheti: %f, %f", cat1, cat2);
        
    [tri release];
}


Выполняем тест и получаем сообщение о пройденном тесте.
Test Suite 'OcuTests' started at 2012-11-23 14:25:39 +0000
Test Case '-[OcuTests testTriangleHypotenuse]' started.
Test Case '-[OcuTests testTriangleHypotenuse]' passed (0.000 seconds).
Test Suite 'OcuTests' finished at 2012-11-23 14:25:39 +0000.
Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds


PS:
Если вам будет интересно прочитать следующую статью, то напишите, пожалуйста, какие примеры для тестирования вы хотели бы увидеть.
Поделиться публикацией

Комментарии 28

    +1
    Хотелось бы увидеть примеры из реальной жизни — совершенно не понятно зачем тестировать сложение чисел.
    Вот к примеру сейчас занимаюсь разработкой одного приложения и не могу даже представить, куда это TDD можно вообще там приткнуть.
      0
      Эта статья написана для новичков которые уже решили, что хотят идти путем TDD. В статье я лишь помогаю делать самые первые шаги, что может быть элементарнее сложения чисел. Я не знаю какое приложение вы разрабатываете, поэтому не могу сказать куда вы можете воткнуть там TDD, может в вашем случае полезно пойти другим путем.
        0
        идти путем TDD

        ну, не только для этого полезно :) для себя лично считаю TDD (в его классическом подходе — сначала тесты, потом реализация методов) излишне «далеко планируемым», но использую юнит-тесты как годную альтернативу рутинному тестированию (собственно, тот же TDD, только вид сбоку), с тем, чтобы убедиться в том, что новые изменения не сломали ранее отлаженных частей.
          0
          Вы меня не поняли. Я имел ввиду то, что было бы хорошо отразить в следующей статье реальное практическое применение юнит-тестов, ведь на Hello-World новичку далеко не уехать.
            0
            Именно в этом направлении я и собираюсь двигаться, но начинать то с чего то надо. Меня инетересует мнение читателей, о чем писать дальше.
            +2
            Ну банально, как оттестировать ViewController какой-нибудь?
              0
              На ваш вопрос могу только один ответ дать — «42»

              Что конкретно оттестировать?
                +1
                Ну что является основным продуктом программирования под iOS, например? Небольшая модель, и много ViewController'ов. Методы, которые скрывают и показывают элементы интерфейса, дергают другие функции (уже на моделях) и т.д… Как mock'ать объекты, как разобраться с тем, что структура view такая, как мне нужна, и т.д.? Как покрыть этот код хотя бы на 90%.
                  +1
                  Хорошо, я подумаю какой пример можно придумать.
                    +1
                    присоединяюсь, очень интересно как протестировать интерфейсные элементы.
                      0
                      Вам нужны UI Automation tests.
                      Первым делом попробуйте стандартное решение, предлагаемое Apple в Instruments (template Automation). Если захочется CI – обратите внимание на Frank или Calabash.
          +1
          Прекрасное введение в юнит-тесты в xcode. Спасибо. Таки юнит тесты удобная штука. Мне кажется, не помешал бы обзор assert'ов — пусть документация по ним и гуглится на раз-два, но вдруг там есть какие-нибудь тонкости использования.

          Ну а в целом было бы интересно почитать особенности внедрения юнит тестирования для объектов посложнее — там использование фейковых заглушек (mock'ов) и пр в применении к разработке для мобильных устройств.
            +2
            Могу сделать урок про создание заглушки для работы с сетью, что бы можно было отработать работу приложения с не валидными данными. Помогите только определится будет ли он кому то интересен.
              0
              Я бы почитал…
                0
                Плюс. Мне тоже интересно.
                  0
                  С удовольствием обучусь. А то все NSLog() да NSLog()…
                    0
                    Думаю вам уже стоит начинать писать!
                  –1
                  > мне надоело, что исправление багов занимает у меня больше времени, чем разработка
                  > предпочитаю не использовать ARC

                  Кажется я знаю, с чем связана первая проблема…
                    +1
                    Намек ваш понял, но тут вас огорчу, исправление багов у меня имеет мало общего с утечками памяти. Скорее когда новый функционал дописываешь незаметно отваливается какой-то старый, вот это отследить я и пытаюсь. Только вот говорить что плохая архитектура изначально не надо. Разрабатываем приложения мы в реальном мире, и финты ушами от заказчиков вещь довольно частая.
                      0
                      По опыту большинство багов — это именно неправильное использование фреймворков (тот-же UIKit), именно в UI части. И вот если вы покажите как использовать Unit-тесты для тестирования не модели, а контроллера, или вью под ios — это будет горазд гораздо ценнее.
                    0
                    как быть с блоками в контексте юнит тестов?
                      0
                      Можете как то развернуть вопрос, я не понял в чем он заключается.
                        0
                        как покрыть код, содержащий блоки, юнит тестами
                          0
                          В чем конкретно проблема, что делается такого страшного в блоках? Вы имеете в виду, что у вас вас в блоках происходят какие-то действия и вы хотите покрыть тестами как эти действия выполняются или что?
                      0
                      Хотелось бы понять границу применимости unit тестов. Тут отметили, есть уже готовые инструменты от Apple для автоматизированного тестирования UI в Instruments (UI Automation tests). Было бы круто услышать чем нужно тестировать интерфейс(например, как можно автоматически, за раз протестировать перемещение по всем вью контроллерам и нажатие всех кнопок, мультитачи, смену ориентации), чем дату, чем связи между классами и т.д.
                        0
                        Дело в том, что в основном интерфейсы у меня рисуются через OpenGL и со стандартными элементами я знаком плохо.
                          +1
                          ARC — довольно топорная технология автоматической подстановки retain/release/autorelease при компиляции с парой интересных (но совершенно непринципиальных) оптимизаций, никакой черной магии. Не знаю, чему там нужно «полежать», все прекрасно работает и избавляет от кучи потенциальных проблем (заметьте, я не говорю, что знать как оно работает под капотом не нужно, наоборот, очень даже нужно). Опять же, при наличии ARC есть автоматически обнуляемые weak-ссылки.
                            0
                            Тестирование классов выполняющих асинхронные действия и тестирование объектов связанных с NSURLRequest, без подключения к интернету

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

                            Самое читаемое