Предисловие
Этот топик не ставит своей целью рассказать о code coverage, и о том, нужно это средство или нет. Также, не будет подниматься вопрос о целесообразности тестов в iOS проектах (положим, что они все-таки кому-то нужы, а значит есть).
Мотивация
Очень удобно, когда средства для профилирования/анализа встроены в IDE. История С code coverage в xCode не совсем безоблачная: во времена xCode 3.x и GCC все было просто и нужные ссылки и флаги компилятора гуглились на раз. C приходом xCode 4.1 все стало немного сложнее ввиду использования LLVM-GCC, приходилось идти на некоторые ухищрения (вплоть до сборки LLVM своими руками). А в 4.3 библиотеку libprofile_rt переместили в другую директорию, что тоже вызвало немало проблем.
Опытным путем было установлено, что для xCode 4.4 настройка code coverage занимает несколько минут, а раз это дешево, то почему бы не использовать? Альтернативный практический вариант использования, проверенный на практике — тестирование непосредственно кода проекта и поиск 'мертвого' кода, с последующим его анализом и чисткой.
Настройка проекта в xCode 4.4
Создадим новый проект (iOS / OS X) c галочкой Include Unit Tests. Можно использовать мой тестовый проект с готовыми юнит-тестами.
Настройка проекта включает два шага:
1. Открываем таргет %project-name%, и выставляем флаги в секции Code generation:
Generate Test Coverage Files = YES
Instrument Program Flow = YES

2. Только для iOS. Во избежание крешей, описаных здесь, необходимо добавить в проект *.c файл со следующим содержимым:
#include <stdio.h> FILE* fopen$UNIX2003(const char* filename, const char* mode); size_t fwrite$UNIX2003(const void* ptr, size_t size, size_t nitems, FILE* stream); FILE* fopen$UNIX2003(const char* filename, const char* mode) { return fopen(filename, mode); } size_t fwrite$UNIX2003(const void* ptr, size_t size, size_t nitems, FILE* stream) { return fwrite(ptr, size, nitems, stream); }
Вот и вся настройка проекта. Теперь, после запуска %projectname%Tests, необходимо открыть директорию (для iOS) "/Users/%user%/Library/Developer/Xcode/DerivedData/%project-nameBLABLABLABLA%/Build/Intermediates/%project-name%.build/Debug-iphonesimulator/%project-name%.build/Objects-normal/i386". В этой директории нас интересуют файлы *.gcda и *.gcno, которые содержат данные о покрытии. Важно: если вы собираетесь тестировать покрытие кода приложения, а не тестов, необходимо в *.plist указать UIApplicationExitsOnSuspend = YES, так как файлы *.gcda создаются только после 'усрешного' завершения работы программы.
Для наглядности привожу код тестируемого класса и несколько тестов:
#import "MyCalc.h" @implementation MyCalc - (CGFloat)performOperation:(MyMathOperation)operation withA:(CGFloat)a B:(CGFloat)b { CGFloat result = 0.f; switch (operation) { case MyMathOperationAdd: result = a + b; break; case MyMathOperationSubtract: result = a - b; break; case MyMathOperationDivide: result = a / b; break; case MyMathOperationMultiply: result = a * b; break; default: NSLog(@"Unsupported operation"); break; } return result; } - (CGFloat)negate:(CGFloat)number { //this method works incorrectly return number; } @end
- (void)testNegation { CGFloat input = 3; CGFloat expected = -3; CGFloat result = [self.calculator negate:input]; STAssertEquals(result, expected, @"Negation failed. Expected: %f, Actual: %f", expected, result); } - (void)testAddition { CGFloat a = 3; CGFloat b = 4; CGFloat expected = a + b; CGFloat result = [self.calculator performOperation:MyMathOperationAdd withA:a B:b]; STAssertEquals(result, expected, @"Addition failed. Expected: %f, Actual: %f", expected, result); } - (void)testMultiplication { CGFloat a = 14; CGFloat b = 3; CGFloat expected = a * b; CGFloat result = [self.calculator performOperation:MyMathOperationMultiply withA:a B:b]; STAssertEquals(result, expected, @"Addition failed. Expected: %f, Actual: %f", expected, result); }
Анализ результатов
Рассмотрим несколько средств для представления статистики в удобном для человека формате.
gcov
gcov — утилита, которая генерирует статистику покрытия на основании файлов *.gcda и *.gcno. До недавнего времени работала только с GCC, в настоящий момент отлично работает и с LLVM. На выходе получаем plain-text отчет.
Для примера, вот результат запуска на файлах MyCalc.gcda из тестового проекта:

На выходе имеем статистику в процентах о покрытии, а также файл MyCalc.m.gcov:
-: 0:Source:/Users/dlebedev/src/sandbox/Coverage/iOS/iOSCoverage/../../Common/MyCalc.m -: 0:Graph:MyCalc.gcno -: 0:Data:MyCalc.gcda -: 0:Runs:0 -: 0:Programs:0 -: 1:// -: 2:// MyCalc.m -: 3:// iOSCoverage -: 4:// -: 5:// Created by Denis Lebedev on 23.08.12. -: 6:// Copyright (c) 2012 Denis Lebedev. All rights reserved. -: 7:// -: 8: -: 9:#import "MyCalc.h" -: 10: -: 11:@implementation MyCalc -: 12: 2: 13:- (CGFloat)performOperation:(MyMathOperation)operation withA:(CGFloat)a B:(CGFloat)b { 2: 14: CGFloat result = 0.f; -: 15: 2: 16: switch (operation) { -: 17: case MyMathOperationAdd: 1: 18: result = a + b; 1: 19: break; -: 20: case MyMathOperationSubtract: #####: 21: result = a - b; -: 22: #####: 23: break; -: 24: case MyMathOperationDivide: #####: 25: result = a / b; -: 26: #####: 27: break; -: 28: case MyMathOperationMultiply: 1: 29: result = a * b; 1: 30: break; -: 31: default: #####: 32: NSLog(@"Unsupported operation"); #####: 33: break; -: 34: } 2: 35: return result; -: 36:} 1: 37:- (CGFloat)negate:(CGFloat)number { -: 38: //this method works incorrectly 1: 39: return number; -: 40:} -: 41: -: 42:@end
#####:- строка не выполнилась.
n: — строка исполнилась n раз.
Более подробно можно почитать здесь.
CoverStory
CoverStory — GUI надстройка над gcov, дополнительно позволяет генерировать html-отчеты с помощью Apple Script.

lcov
lcov — еще один графический фронт-енд для gcov. Очень удобен при наличии большого количества файлов, так как группирует html по директориям, а также при автоматизации процесса — утилита работает из терминала.
Установка lcov:
# sudo mkdir -p /usr/local/src; cd /usr/local/src # sudo wget http://downloads.sourceforge.net/ltp/lcov-1.6.tar.gz # sudo tar -xzvf lcov-1.6.tar.gz # cd lcov-1.6
# sudo vim /usr/local/src/lcov-1.6/bin/install.sh</code>
В строке 34 (install -D $SOURCE $TARGET) необходимо удалить флаг -D.
# sudo make install
Для получения отчета выполним следующие команды в папке с *.gcda-файлами:
lcov -t 'Code coverage report' -o report.info -c -d . genhtml -o html-report report.info
Результат в папке html-report:

Автоматизация
Как уже упоминалось выше, lcov удобен в continuos integration. Демонстрируемый пример будет исключительно академическим и с досадными недостатками (так и не удалось применить скрипт к проекту, использующему CocoaPods).
Код скрипта (его также можно найти в папке с тестовым iOS проектом):
#!/bin/sh TARGET_NAME="iOSCoverage" TEST_TARGET_NAME="iOSCoverageTests" BUILD_CONFIG="Debug" SDK_VERSION="iphonesimulator5.1" rm -rf build rm -rf html-report echo Building and running tests xcodebuild -target $TEST_TARGET_NAME OBJECT_FILE_DIR_normal=/build/ TEST_AFTER_BUILD=YES -sdk iphonesimulator5.1 -configuration $BUILD_CONFIG -xcconfig settings.xcconfig echo Copying files mkdir build/gcda cp build/$TARGET_NAME.build/$BUILD_CONFIG-iphonesimulator/$TARGET_NAME.build/Objects-normal/i386/*.gcda build/gcda/ cp build/$TARGET_NAME.build/$BUILD_CONFIG-iphonesimulator/$TARGET_NAME.build/Objects-normal/i386/*.gcno build/gcda/ echo Generating report cd build/gcda lcov -t 'Code coverage report' -o report.info -c -d . cd .. cd .. genhtml -o html-report build/gcda/report.info
Содержимое settings.xcconfig (в проекте можно не прописывать флаги):
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES GCC_GENERATE_TEST_COVERAGE_FILES = YES
Кладем скрипт и файл settings.xcconfig рядом с проектом (предварительно заменяем переменные названий таргетов и SDK на нужные), запускаем… и получаем ошибку. Так как изначально iPhone Simulator не умеет запускать тесты из командной строки. Как починить это досадное недоразумение, описано здесь. После этого, запускаем скрипт еще раз и получаем папку html-report со статистикой.
UPD: Для боле�� тесной интеграции с Jenkins можно использовать gcovr + Cobertura Plugin . Спасибо moborb за наводку.
