Данная статья посвящена вопросу тестирования в рамках Objective-C используя Xcode 6. Рассматриваются стандартная библиотека для тестирования и сторонняя библиотека OCMock. Опытные разработчики, возможно, не найдут здесь слишком полезной информации, тем же, кто недавно встал на этот путь — статья откроет необходимые базовые знания по написанию unit-тестов на языке Objective-C.

Для основы тестирования просьба обратиться сюда.
Для основы unit-тестирования сюда.

А теперь мы начнем изучение unit-тестирования в рамках Objective-C.

Шаг 1. Основы


Создадим новый iOS проект.

Скриншоты




Xcode, как мы видим, сам создал для нас каталог для тестов DemoUnitTestingTests и файл DemoUnitTestingTests.m. Что мы тут видим:

Стандартную библиотеку для тестирования:

#import <XCTest/XCTest.h>

То, что наш класс DemoUnitTestingTests наследуется от класса XCTestCase (не будем вдаваться в подробности).

Метод вызываемый перед запуском каждого теста:

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

Метод вызываемый после результата каждого теста:

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

И непосредственно 2 теста. Первый — демонстрационный тест, второй — для демонстрации теста производительности:

- (void)testExample {
    // This is an example of a functional test case.
    XCTAssert(YES, @"Pass");
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

Как можно заметить, все тесты начинаются со слова test и автоматически включаются в список для тестирования.

Шаг 2. Информация


Немного правил и замечаний:
Все тесты запускаются независимо друг от друга
Делайте максимальную изоляцию класса
Название теста должно отображать его назначение
Результат работы теста не должен влиять на основной код

Пожалуйста, не стоит думать, что написание тестов избавит ваш код от багов раз и навсегда. Возможно, ваш тест написан не вполне корректно.

Шаг 3. Приступаем к изучению тестов


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

Всегда ошибка
- (void)testAlwaysFailed {
    /*
     аргумент1 - текст ошибки
     */
    XCTFail(@"always failed");
}


Равенство базовых типов
- (void)testIsEqualPrimitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат для вывода ошибки
     */
    int primitive1 = 5;
    int primitive2 = 5;
    XCTAssertEqual(primitive1, primitive2, @"(%d) equal to (%d)", primitive1, primitive2);
}


Неравенство базовых типов
- (void)testIsNotEqualPrimitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат для вывода ошибки
     */
    int primitive1 = 5;
    int primitive2 = 6;
    XCTAssertNotEqual(primitive1, primitive2, @"(%d) not equal to (%d)", primitive1, primitive2);
}


Равенство с погрешностью базовых типов
- (void)testIsEqualWithAccuracyPrimitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     аргумент3 - величина допустимой погрешности
     необязательно
     аргумент4 - формат вывода
     последующие - аргументы в формат для вывода ошибки
     */
    float primitive1 = 5.012f;
    float primitive2 = 5.014f;
    float accuracy = 0.005;
    XCTAssertEqualWithAccuracy(primitive1, primitive2, accuracy, @"(%f) equal to (%f) with accuracy %f", primitive1, primitive2, accuracy);
}


Неравенство с погрешностью базовых типов
- (void)testIsNotEqualWithAccuracyPrimitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     аргумент3 - величина допустимой погрешности
     необязательно
     аргумент4 - формат вывода
     последующие - аргументы в формат для вывода ошибки
     */
    float primitive1 = 5.012f;
    float primitive2 = 5.014f;
    float accuracy = 0.001;
    XCTAssertNotEqualWithAccuracy(primitive1, primitive2, accuracy, @"(%f) not equal to (%f) with accuracy %f", primitive1, primitive2, accuracy);
}


Проверка на BOOL YES
- (void)testIsTrue {
    /*
     аргумент1 - boolean либо приведение
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    BOOL isTrue = YES;
    XCTAssertTrue(isTrue);
}


Проверка на BOOL NO
- (void)testIsFalse {
    /*
     аргумент1 - boolean либо приведение
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    BOOL isTrue = NO;
    XCTAssertFalse(isTrue);
}


Проверка на nil
- (void)testIsNil {
    /*
     аргумент1 - указатель
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    id foo = nil;
    XCTAssertNil(foo, @"pointer:%p", foo);
}


Проверка на НЕ nil
- (void)testIsNotNil {
    /*
     аргумент1 - указатель
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    id foo = @"";
    XCTAssertNotNil(foo);
}


Сравнение примитивов на больше (>)
- (void)testGreaterPrivitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    int privitive1 = 4;
    int privitive2 = 3;
    XCTAssertGreaterThan(privitive1, privitive2);
}


Сравнение примитивов на больше или равно (>=)
- (void)testGreaterOrEqualPrivitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    int privitive1 = 4;
    int privitive2 = 4;
    XCTAssertGreaterThanOrEqual(privitive1, privitive2);
}


Сравнение примитивов на меньше (<)
- (void)testLessPrivitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    int privitive1 = 3;
    int privitive2 = 4;
    XCTAssertLessThan(privitive1, privitive2);
}


Сравнение примитивов на меньше или равно (<=)
- (void)testLessOrEqualPrivitive {
    /*
     аргумент1 - примитив1
     аргумент2 - примитив2
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    int privitive1 = 4;
    int privitive2 = 4;
    XCTAssertLessThanOrEqual(privitive1, privitive2);
}


Проверка на выбрасывание исключения
- (void)testThrowException {
    /*
     аргумент1 - блок/метод/функция
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    void (^block)() = ^{
        @throw [NSException exceptionWithName:NSGenericException
                                       reason:@"test throw"
                                     userInfo:nil];
    };
    XCTAssertThrows(block());
}


Проверка на НЕ выбрасывание исключения
- (void)testNoThrowException {
    /*
     аргумент1 - блок/метод/функция
     необязательно
     аргумент2 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    void (^block)() = ^{
    };
    XCTAssertNoThrow(block());
}


Проверка на выбрасывание исключения классом исключения
Класс MyException производный от NSException
@interface MyException : NSException
@end

@implementation MyException
@end

- (void)testThrowExceptionClass {
    /*
     аргумент1 - блок/метод/функция
     аргумент2 - класс исключения
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    void (^block)() = ^{
        @throw [MyException exceptionWithName:NSGenericException
                                       reason:@"test throw"
                                     userInfo:nil];
    };
    XCTAssertThrowsSpecific(block(), MyException);
}


Проверка на выбрасывание исключения ОТЛИЧНЫМ от класса исключения
Класс MyException производный от NSException
@interface MyException : NSException
@end

@implementation MyException
@end

- (void)testNoThrowExceptionClass {
    /*
     аргумент1 - блок/метод/функция
     аргумент2 - класс исключения
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    void (^block)() = ^{
        @throw [NSException exceptionWithName:NSGenericException
                                       reason:@"test throw"
                                     userInfo:nil];
    };
    XCTAssertNoThrowSpecific(block(), MyException);
}


Проверка на выбрасывание исключения классом исключения с именем
Класс MyException производный от NSException
@interface MyException : NSException
@end

@implementation MyException
@end

- (void)testThrowWithNamedExceptionClass {
    /*
     аргумент1 - блок/метод/функция
     аргумент2 - класс исключения
     аргумент3 - имя ошибки
     необязательно
     аргумент4 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    NSString *nameException = @"name expection";
    void (^block)() = ^{
        @throw [MyException exceptionWithName:nameException
                                       reason:@"test throw"
                                     userInfo:nil];
    };
    XCTAssertThrowsSpecificNamed(block(), MyException, nameException);
}


Проверка на выбрасывание исключения ОТЛИЧНЫМ от класса исключения с именем
Класс MyException производный от NSException
@interface MyException : NSException
@end

@implementation MyException
@end

- (void)testNoThrowWithNamedExceptionClass {
    /*
     аргумент1 - блок/метод/функция
     аргумент2 - класс исключения
     аргумент3 - имя ошибки
     необязательно
     аргумент4 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    NSString *nameException = @"name expection";
    void (^block)() = ^{
        @throw [MyException exceptionWithName:[nameException stringByAppendingString:@"123"]
                                       reason:@"test throw"
                                     userInfo:nil];
    };
    XCTAssertNoThrowSpecificNamed(block(), MyException, nameException);
}


Равенство объектов
- (void)testEqualObject {
    /*
     аргумент1 - объект реализующий isEqual
     аргумент2 - объект реализующий isEqual
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    id obj1 = @[];
    id obj2 = @[];
    XCTAssertEqualObjects(obj1, obj2, @"obj1(%@) not equal to obj2(%@))", obj1, obj2);
}


Неравенство объектов
- (void)testNoEqualObject {
    /*
     аргумент1 - объект реализующий isEqual
     аргумент2 - объект реализующий isEqual
     необязательно
     аргумент3 - формат вывода
     последующие - аргументы в формат вывода ошибки
     */
    id obj1 = @"name";
    id obj2 = @{};
    XCTAssertNotEqualObjects(obj1, obj2, @"obj1(%@) not equal to obj2(%@))", obj1, obj2);
}


Проверка с задержкой по времени
- (void)testAsync {
    /*
     1. Создаем ожидание с описанием (будет показано в случае провала теста)
     2. Выполняем необходимые действия
     3. Устанавливаем ожидание
     4. Вызываем fulfill метод у объекта класса XCTestExpectation
     Если сперва вызвать fulfill и потом установить ожидание - тест будет считаться пройденным
     */
    XCTestExpectation *expectation = [self expectationWithDescription:@"block not call"];
    NSTimeInterval timeout = 1.0f;
    [expectation performSelector:@selector(fulfill)
                      withObject:nil
                      afterDelay:0.3f];
    [self waitForExpectationsWithTimeout:timeout
                                 handler:nil];
}


Шаг 4. Тестирование кода с зависимостями


Подошли к самой интересной части статьи. Если вы внимательно прочитали 2 первые ссылки, то уже знаете, что для unit-тестирования необходимо изолировать класс от зависимостей. Можно написать все руками, но мы возьмем готовое. Возьмем библиотеку OCMock.

Установим, используя CocoaPods:
pod 'OCMock', '~> 3.1.2'

Рассмотрим самые частые проблемы при написании тестов, когда есть зависимости, и посмотрим как с ними справиться, используя библиотеку OCMock. Создадим для демонстрации классы ClassA и ClassB.

Зависимость от другого класса
- (void)testInit {
    /*
     Создадим mock объект класса ClassB и передадим его в init класса ClassA
     Проверим, что classA.classB указывает на тот же адрес, что и mockClassB
     */
    id mockClassB = OCMClassMock([ClassB class]);
    ClassA *classA = [[ClassA alloc] initWithClassB:mockClassB];
    XCTAssertEqual(classA.classB, mockClassB);
}

Данные от другого объекта
- (void)testStub {
    /*
     Создадим mock объект класса ClassB и напишем stub метод, возвращающий ожидаемые данные.
     Настоящий же метод в классе ClassB будет выбрасывать исключение
     */
    NSString *expectedInfo = @"info";
    id mockClassB = OCMClassMock([ClassB class]);
    OCMStub([mockClassB info]).andReturn(expectedInfo);

    NSString *info = [mockClassB info];
    XCTAssertEqualObjects(info, expectedInfo);
}

Данные от другого объекта в зависимости от входных данных
- (void)testStubWithArg {
    /*
     Создадим mock объект класса ClassB и напишем stub метод, зависимый от входных данных
     Проверим возвращаемые значения
     */
    id mockClassB = OCMClassMock([ClassB class]);
    NSInteger expectedFactorial3 = 6;
    NSInteger expectedFactorial5 = 120;
    OCMExpect([mockClassB factorial:3]).andReturn(expectedFactorial3);
    OCMExpect([mockClassB factorial:5]).andReturn(expectedFactorial5);
    
    NSInteger factorial3 = [mockClassB factorial:3];
    NSInteger factorial5 = [mockClassB factorial:5];
    
    XCTAssertEqual(factorial3, expectedFactorial3);
    XCTAssertEqual(factorial5, expectedFactorial5);
}

Нотификация от другого объект
- (void)testNotification {
    /*
     Создадим mock объект класса ClassB и напишем stub метод, посылающий нотификацию
     Создадим mock observer для нотификации
     Создадим ожидание срабатывания нотификации от mock объекта
     Проверим сработали ли все созданные ожидания для mock observer
     */
    id mockClassB = OCMClassMock([ClassB class]);
    NSString *notificationName = @"notification name";
    NSNotification *notification = [NSNotification notificationWithName:notificationName
                                                                 object:mockClassB];
    OCMStub([mockClassB postNotification]).andPost(notification);
    
    id mockObserver = OCMObserverMock();
    [[NSNotificationCenter defaultCenter] addMockObserver:mockObserver
                                                     name:notificationName
                                                   object:mockClassB];
    OCMExpect([mockObserver notificationWithName:notificationName
                                          object:mockClassB]);
    
    [mockClassB postNotification];
    OCMVerifyAll(mockObserver);
}


Вызов метода другого объекта
Не отличается по логике от примера выше. Пишем ожидания и проверяем были ли они вызваны
- (void)testVerifyExpect {
    /*
     Создадим mock объект класса ClassB и напишем ожидания вызова методов info и postNotification
     Проверим сработали ли все созданные ожидания для mock объекта
     */
    id mockClassB = OCMClassMock([ClassB class]);
    OCMExpect([mockClassB info]);
    OCMExpect([mockClassB postNotification]);
    [mockClassB info];
    [mockClassB postNotification];
    OCMVerifyAll(mockClassB);
}


Зависимости от текущего состояния объекта
- (void)testPartialMockObject {
    /*
     Создадим объект класса ClassA, изменим для теста состояние поля count.
     Поле count будет readonly
     */
    id classA = [[ClassA alloc] initWithCount:3];
    XCTAssertEqual([classA count], 3);
    
    id partialMock = OCMPartialMock(classA);
    OCMStub([partialMock count]).andReturn(41);
    XCTAssertEqual([classA count], 41);
}


Вызов у объекта блока с задержкой
- (void)testStubWithBlock {
    /*
     Создадим mock объект класса ClassB и напишем stub метод, принимающий блок и вызывающий его
     Создадим mock объект класса ClassA и напишем ожидание вызова блока
     Убедимся, что вызов блока произошел в некий допустимый промежуток времени
     */
    void (^block)() = [OCMArg checkWithBlock:^BOOL(id value) {
        return YES;
    }];
    id mockClassB = OCMClassMock([ClassB class]);
    OCMExpect([mockClassB setBlock:block]);

    id mockClassA = OCMClassMock([ClassA class]);
    OCMStub([mockClassA useBlockInClassB]).andDo(^(NSInvocation *invocation) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [mockClassB setBlock:^{
            }];
        });
    });

    [mockClassA useBlockInClassB];
    OCMVerifyAllWithDelay(mockClassB, 2);
}


Для большинства задач этого должно хватить. Более подробно предлагаю прочитать здесь.

Шаг 5. Настройка запуска тестов логики


При запуске тестов (к примеру комбинацией cmd+u) сперва запускается основной код и только после этого тесты. Это плохо. Часто бывают ситуации, когда программа при выполнении основного кода падает и тесты не запускаются. Исправим это.

Создадим еще один AppDelegate (AppDelegateForTest), но для запуска тестов (при создании выбираем добавить в основной Target).

Изменим main.c
int main(int argc, char *argv[]) {
    @autoreleasepool {
        Class appDelegateClass = (NSClassFromString(@"XCTestCase") ? [TestingSAAppDelegate class] : [SAAppDelegate class]);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
    }
}


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

Думаю, на этом статью можно закончить. Были рассмотрены функции для тестирования из стандартной библиотеки, а так же самое необходимое из библиотеки OCMock. Исходник проекта можно скачать здесь.

Удачного вам тестирования!

Комментарии (11)


  1. IbrahimKZ
    28.05.2015 06:58

    А когда же начнется само unit-тестирование?

    Я имею ввиду, что Вы просто описали каждую функцию и для чего она. Но начинающим не хватает какого-нибудь мало-мальски работающего приложения (небольшого), где уже НА ПРАКТИКЕ показали бы, как используется это unit-тестирование. Где был бы разобран каждый вид (Равенство базовых типов, Проверка на nil, Проверка на выбрасывание исключения и т.д.). Хотя, наверно, в рамках одной статьи такое тяжело написать. Надо маленькую книжку написать.

    Никак не могу начать писать unit-тесты для своих приложений, потому что не могу понять вообще зачем оно мне и как, и для чего мне их писать. Именно на практике.


    1. ajjnix Автор
      28.05.2015 07:59

      В данный момент график сильно забит (данная статья была написана еще в воскресенье), но чуть позднее (предположительно под конец июня) смогу сесть и написать разбор небольшого приложения под iOS, написанного через тестирование (TDD). Главное нужно будет придумать, что должно быть в этом приложении, чтобы не пришлось объяснять помимо тестирование моменты.

      p.s. А вы скачивали исходники проекта? Кроме описания каждой функции, есть так же небольшой пример использования.


      1. IbrahimKZ
        28.05.2015 08:06

        Хорошо, будем ждать. Надеюсь, не только мне это пригодится.

        p.s. Скачал. На само приложение не делает ровным счетом ничего :-)


        1. i_user
          28.05.2015 08:44

          P.S. Людям, которые пока еще не вкурили TDD я предлагаю не пытаться начинать свой опыт в тестировании именно с него. TDD — довольно спорная практика и, как правило, на реальных проектах при сильной изменчивости требований в начале разработки от нее больше вреда, чем пользы.

          А тестировать свой код надо. Почему надо?
          1. Банальное сравнение. Для того, чтобы проверить свой код без тестов — вам надо запустить проект, дощелкать до нужного места (на одном из проектов, где я работал, это занимало порядка полутора минут), после этого проверить требуемый кейс. В то время как собственно тест займет в худшем случае секунд 15 (при правильной настройке окружения)
          2. Более того мало у кого хватит терпения при таком раскладе проверить все возможные ветки исполнения кода (особенно, если это касается edge-кейсов, что программа работает корректно, когда что-то идет не так)

          Так что предлагаю вам начать с простого. Настройте тестовое окружение и на любую чисто логическую функциональность напишите тест. В практически любой программе есть 1-2 чисто функциональных модуля, которые берут какие-то данные и что-то с ними делают. Да, поначалу это будет казаться довольно очевидным, но это пройдет довольно быстро :-)

          P.S. посмотрите репозитории по ссылкам в комментарии ниже — там можно найти довольно много вдохновения по тестам.


          1. IbrahimKZ
            28.05.2015 08:47

            Спасибо. Обязательно посмотрю.


    1. GigabyteTheOne
      02.06.2015 06:04
      +1

      Есть неплохая книга «Грэхем Ли — Разработка через тестирование для iOS», там всё на примере приложения расписано.


      1. IbrahimKZ
        02.06.2015 09:11

        Актуальна ли книга?
        Глянул. Она еще в 2012 году была написана, в ней Xcode 4 и, вероятно, iOS 5 рассматривается.
        Или это не помеха?


        1. ajjnix Автор
          02.06.2015 09:17
          +1

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


        1. GigabyteTheOne
          02.06.2015 09:17
          +2

          Вполне актуальна, в тестах вроде бы с тех времён ничего особо не поменялось. Для общего понимания подходит на 100%


  1. i_user
    28.05.2015 08:30
    +2

    В этой статье вы разбирали XCTest, в основном.
    Возможно, вы итак в курсе, но я бы предложил (например, читателям) поинтересоваться такой концепцией как BDD
    В мире мобильной разработки она сейчас больше удовлетворяет нуждам. Под обжектив BDD фреймворком является expecta, как надстройка над specta
    Под Свифт — это пара Nimble/Quick которая реализует примерно такую же функциональность.

    Значительными преимуществами BDD перед TDD является включенное by-design описание тестовых сценариев, соответственно вернувшись к тестам через год, тебе по прежнему будет легко с ними работать. Кроме этого, BDD фреймворки захватывают помимо, непосредственно, юнит тестов еще и часть соседнего домена — интеграционных тестов. Соответственно связки Expecta + Cucumber хватает для практически полного покрытия приложения тестами.

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

    1. Facebook iOS SDK — тут мало юнит тестов, но много моков и интеграционных тестов
    2. Eigen — этим проектом вообще стоит поинтересоваться с точки зрения организации кода и рабочего процесса, невероятно ценный репозиторий
    3. РАК — просто замечательно протестированный код.

    P.P.S автору рекомендую подумать над тестированием Swift-кода в качестве умственного эксперимента — тестировать код без нормального мокирования гораздо сложнее и гораздо больше способствует самодисциплине в организации кода. Моки в этом плане в Objective-C немного расхолаживают программистов


  1. prizzrak
    28.05.2015 20:59

    Может у кого-то есть опыт юнит тестирования (ocmock, specta) кода в котором есть ReactiveCocoa код?