Pull to refresh

Да начнется unit-тестирование (Objective-C)

Reading time 10 min
Views 36K
Данная статья посвящена вопросу тестирования в рамках 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. Исходник проекта можно скачать здесь.

Удачного вам тестирования!
Tags:
Hubs:
+14
Comments 11
Comments Comments 11

Articles