Pull to refresh

Юнит-тесты в Cocoa

Reading time 5 min
Views 11K
Ниже описаны основы использования OCUnit — фреймворка для создания юнит-тестов, интегрированного в Xcode. Чтобы наглядно попробовать описываемые вещи, код можно скачать сразу. Писал до эпохи Xcode 4, поэтому картинки немного устарели.



Итак, например, у нас есть некая категория, которая расширяет стандартный класс NSString и добавляет метод по перевертыванию своего содержимого:

//ExtendedString.h

#import <Cocoa/Cocoa.h>
 
@interface NSString (Extended)
 
- (NSString *)invert;
 
@end

//ExtendedString.m

#import "ExtendedString.h"
 
@implementation NSString (Extended)
 
- (NSString *)invert
{
    NSUInteger length = [self length];
    NSMutableString *invertedString = [NSMutableString stringWithCapacity: length];
 
    while (length > (NSUInteger)0)
    { 
        unichar c = [self characterAtIndex: --length]; 
        [invertedString appendString: [NSString stringWithCharacters: &c length: 1]];
    }
 
    return invertedString;
}
 
@end

Добавляем к проекту новый таргет. Пусть он называется «Tests»:





Добавляем в проект специальный фреймворк для юнит-тестов — SenTestingKit:



Его нужно добавить только к таргету Tests:



Создаем группу для исходников с названием Tests и добавляем в нее новые тест-файлы. К стати, в папке с проектом лучше всего тоже создать физическую подпапку Tests и там уже размещать исходники с тестами:





Назовем его понятным образом — ExtendedString_Test.m — и добавим к таргету Tests:



Кроме того, к этому таргету Tests нужно будет присовокупить исходники самого тестируемого класса, а не только тестов. В итоге, структура таргетов в проекте будет выглядеть так:



Приведем код тестов, снабдив его необходимыми комментариями:

//ExtendedString_Test.h

#import <SenTestingKit/SenTestingKit.h>  //используем тестовый фреймворк
  
//а вот если бы мы сделали не категорию, а нормальный класс,
//то на месте этого комментария следовало бы написать что-то типа:
//@class MyClass;
//и использовать MyСlass внутри объявления нижеследующего ExtendedString_Test
//там, где сейчас NSString
  
@interface ExtendedString_Test : SenTestCase  //все тест-классы обязаны наследовать это
{
    NSString *_string;
}
 
@end

//ExtendedString_Test.m

#import "ExtendedString_Test.h"
#import "ExtendedString.h"  //не забудем про подопытный класс
 
@implementation ExtendedString_Test
 
- (void)setUp  //это запускается первым, можно настроить подопытные классы и посоздавать объекты
{
    _string = [[NSString alloc] initWithString: @"Hello world!"];
 
    STAssertNotNil(_string, @"Construct error!");  //сразу проверяем, все ли в порядке
}
 
- (void)tearDown  //это запускается последним, можно удалить объекты
{
    [_string release];
}
 
- (void)testInvertString  //это обычный тестовый метод
{
    STAssertEqualObjects(@"Hello world!", _string, @"String is not initialized!");  //проверяем оригинал
    STAssertEqualObjects(@"!dlrow olleH", [_string invert], @"String is not inverted!");  //проверяем перевертыш
}
 
@end

Если все ОК, то сборка таргета Tests просто пройдет успешно. Если какой-то из ассертов окажется невалидным, во время сборки будет показана ошибка, как если бы это была ошибка компиляции. Примерно так:



Теперь, как эта кухня работает?

Во время сборки таргета с тестами создается специальный бандл с расширением octest. Следующем шагом в сборке этого таргета стоит запуск скрипта следующего содержания:

${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests
Эта штука в свою очередь запускает вложенные скрипты, но все, в конечном итоге, сводится к вызову /Developer/Tools/otest с определенными параметрами. Последний загружает в себя наш тестовый бандл, находит там классы-наследники SenTestCase и дергает за метод setUp в начале, tearDown в конце, а между ними он вызывает все методы, название которых начинается со слова «test». Да-да, Objective-C такое позволяет :)

Поэтому все тесты нужно начинать с приставки «test».

К стати, octest можно запустить и вручную. Например так:

export OBJC_DISABLE_GC=YES #выключим Garbage Collector
arch -i386 /Developer/Tools/otest ~/InvertString/build/Debug/Tests.octest #укажем явно архитектуру (i386), а то может ругаться

Вывод будет примерно такой:

objc[22721]: GC: forcing GC OFF because OBJC_DISABLE_GC is set
objc[22721]: GC: forcing GC OFF because OBJC_DISABLE_GC is set
Test Suite '/Users/ium/InvertString/build/Debug/Tests.octest(Tests)' started at 2011-07-01 18:46:45 +0300
Test Suite 'ExtendedString_Test' started at 2011-07-01 18:46:45 +0300
/Users/ium/InvertString/Tests/ExtendedString_Test.m:21: error: -[ExtendedString_Test testInvertString] : 'Hello world' should be equal to 'Hello world!' String is not initialized!
2011-07-01 18:46:45.240 otest[22721:80f] !dlrow olleH
/Users/ium/InvertString/Tests/ExtendedString_Test.m:22: error: -[ExtendedString_Test testInvertString] : '!dlrow olle' should be equal to '!dlrow olleH' String is not inverted!
Test Case '-[ExtendedString_Test testInvertString]' failed (0.003 seconds).
Test Suite 'ExtendedString_Test' finished at 2011-07-01 18:46:45 +0300.
Executed 1 test, with 2 failures (0 unexpected) in 0.003 (0.003) seconds

Test Suite '/Users/ium/InvertString/build/Debug/Tests.octest(Tests)' finished at 2011-07-01 18:46:45 +0300.
Executed 1 test, with 2 failures (0 unexpected) in 0.003 (0.010) seconds

Ну, это для варианта с ошибками, естественно. Оно все вываливается в лог, к стати (Build > Build Results).

Получив такое на sdterr во время сборки, Xcode немедленного его парсит, находит ключевое слово «error:» c предваряющими его именем файла и номером строки, и красиво подсвечивает нам ошибки, как на предыдущей картинке.

Набор макросов для проверки утверждений следующий:

STFail(description, ...)  //это просто способ вывести сообщение об ошибке, очень полезно, где ассертами не обойтись
STAssertNil(a1, description, ...)
STAssertNotNil(a1, description, ...)
STAssertTrue(expression, description, ...)
STAssertFalse(expression, description, ...)
STAssertEqualObjects(a1, a2, description, ...)  //эти объекты равны по содержанию, копии друг друга
STAssertEquals(a1, a2, description, ...)  //это физически один объект
STAssertEqualsWithAccuracy(left, right, accuracy, description, ...)
STAssertThrows(expression, description, ...)
STAssertThrowsSpecific(expression, specificException, description, ...)
STAssertThrowsSpecificNamed(expr, specificException, aName, description, ...)
STAssertNoThrow(expression, description, ...)
STAssertNoThrowSpecific(expression, specificException, description, ...)
STAssertNoThrowSpecificNamed(expr, specificException, aName, description, ...)
STAssertTrueNoThrow(expression, description, ...)
STAssertFalseNoThrow(expression, description, ...)

Названия говорят сами за себя, кроме прокомментированных. Троеточие работает как подстановка параметров в форматную строку description. Там как обычно, знак процента.

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

Удачи!
Tags:
Hubs:
+14
Comments 5
Comments Comments 5

Articles