Генерировать gcda файлы с Xcode5, iOS7-симулятором и XCTest
Будучи вдохновленным решением по этому вопросу, я попытался использовать тот же подход с XCTest.
Я установил "Generate Test Coverage Files = YES" и "Flow Program Program = YES".
XCode по-прежнему не создает никаких файлов gcda. У кого-нибудь есть идеи, как это решить?
код:
#import <XCTest/XCTestLog.h>
@interface VATestObserver : XCTestLog
@end
static id mainSuite = nil;
@implementation VATestObserver
+ (void)initialize {
[[NSUserDefaults standardUserDefaults] setValue:@"VATestObserver"
forKey:XCTestObserverClassKey];
[super initialize];
}
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (mainSuite == suite) {
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
}
@end
В AppDelegate.m У меня есть:
extern void __gcov_flush(void);
- (void)applicationWillTerminate:(UIApplication *)application {
__gcov_flush();
}
EDIT: я отредактировал вопрос, чтобы отразить текущий статус (без красных сельдей).
EDIT Чтобы сделать это, мне пришлось добавить все тестируемые файлы к тестовой цели, включая VATestObserver.
AppDelegate.m
#ifdef DEBUG
+ (void)initialize {
if([self class] == [AppDelegate class]) {
[[NSUserDefaults standardUserDefaults] setValue:@"VATestObserver"
forKey:@"XCTestObserverClass"];
}
}
#endif
VATestObserver.m
#import <XCTest/XCTestLog.h>
#import <XCTest/XCTestSuiteRun.h>
#import <XCTest/XCTest.h>
// Workaround for XCode 5 bug where __gcov_flush is not called properly when Test Coverage flags are set
@interface VATestObserver : XCTestLog
@end
#ifdef DEBUG
extern void __gcov_flush(void);
#endif
static NSUInteger sTestCounter = 0;
static id mainSuite = nil;
@implementation VATestObserver
+ (void)initialize {
[[NSUserDefaults standardUserDefaults] setValue:@"VATestObserver"
forKey:XCTestObserverClassKey];
[super initialize];
}
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
sTestCounter++;
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
sTestCounter--;
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (sTestCounter == 0) {
__gcov_flush();
}
}
Ответы
Ответ 1
Обновление 1:
Прочитав немного больше об этом, мне стало ясно 2 вещи (выделено мной):
Тесты и тестируемое приложение скомпилируются отдельно. Тесты фактически вводятся в запущенное приложение, поэтому __gcov_flush()
необходимо вызвать внутри приложения, не входящего в теги.
- Покрытие кода Xcode5 (из cmd-строки для сборки CI) - Переполнение стека
и
Опять же: инъекция сложна. Ваш урон должен быть: Не добавляйте файлы .m из вашего приложения в тестовый объект. Вы получите неожиданное поведение.
- Контрольные контроллеры просмотра - # 1 - Контроллеры с подсветкой
Код ниже был изменен, чтобы отразить эти две идеи...
Обновление 2:
Добавлена информация о том, как сделать эту работу для статических библиотек по запросу @MdaG в комментариях. Основные изменения для библиотек:
-
Мы можем скрыться непосредственно из метода -stopObserving
, потому что нет отдельного приложения, где нужно вводить тесты.
-
Мы должны зарегистрировать наблюдателя в методе +load
, потому что к моменту вызова +initialize
(когда класс сначала получает доступ из набора тестов) уже слишком поздно, чтобы XCTest его забирал.
Решение
Другие ответы здесь очень помогли мне в создании кода в моем проекте. Изучая их, я считаю, что мне удалось немного упростить код для исправления.
Учитывая одно из:
-
ExampleApp.xcodeproj
создан с нуля как "Пустое приложение"
-
ExampleLibrary.xcodeproj
создается как независимая "Cocoa сенсорная статическая библиотека"
Это были шаги, которые я предпринял для включения генерации кода в Xcode 5:
-
Создайте файл GcovTestObserver.m
со следующим кодом внутри группы ExampleAppTests:
#import <XCTest/XCTestObserver.h>
@interface GcovTestObserver : XCTestObserver
@end
@implementation GcovTestObserver
- (void)stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
@end
Когда вы делаете библиотеку, так как нет приложения для вызова, флеш может быть вызван непосредственно от наблюдателя. В этом случае добавьте этот файл в группу ExampleLibraryTests с помощью этого кода:
#import <XCTest/XCTestObserver.h>
@interface GcovTestObserver : XCTestObserver
@end
@implementation GcovTestObserver
- (void)stopObserving
{
[super stopObserving];
extern void __gcov_flush(void);
__gcov_flush();
}
@end
-
Чтобы зарегистрировать класс тестового наблюдателя, добавьте следующий код в раздел @implementation
любого из следующих:
-
ExampleAppDelegate.m
, внутри группы ExampleApp
-
ExampleLibrary.m
, внутри группы ExampleLibrary
#ifdef DEBUG
+ (void)load {
[[NSUserDefaults standardUserDefaults] setValue:@"XCTestLog,GcovTestObserver"
forKey:@"XCTestObserverClass"];
}
#endif
Ранее этот ответ предложил использовать метод +initialize
(и вы все еще можете это сделать в случае приложений), но он не работает для библиотек...
В случае библиотеки +initialize
, вероятно, будет выполняться только тогда, когда тесты впервые вызовут код библиотеки, и к тому времени уже слишком поздно регистрировать наблюдателя. Используя метод +load
, регистрация наблюдателя всегда выполняется вовремя, независимо от того, какой сценарий.
-
В случае приложений добавьте следующий код в раздел @implementation
файла ExampleAppDelegate.m
внутри группы ExampleApp, чтобы очистить файлы покрытия при выходе из приложения
- (void)applicationWillTerminate:(UIApplication *)application
{
#ifdef DEBUG
extern void __gcov_flush(void);
__gcov_flush();
#endif
}
-
Включите Generate Test Coverage Files
и Instrument Program Flow
, установив их в YES
в настройках сборки проекта (для целей "Пример" и "Пример тестов" ).
Чтобы сделать это легко и последовательно, я добавил файл Debug.xcconfig
связанный с конфигурацией проекта "Отладка" , со следующими объявлениями:
GCC_GENERATE_TEST_COVERAGE_FILES = YES
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
-
Убедитесь, что все файлы проекта .m
также включены в фазу сборки "Источники компиляции" целевой цели "Примеры тестов". Не делайте этого: код приложения принадлежит к целевому приложению, тестовый код принадлежит тестовой цели!
После запуска тестов для вашего проекта вы сможете найти созданные файлы покрытия для Example.xcodeproj
здесь:
cd ~/Library/Developer/Xcode/DerivedData/
find ./Example-* -name *.gcda
Примечания
Шаг 1
Объявление метода внутри XCTestObserver.h
указывает:
/*! Sent immediately after running tests to inform the observer that it time
to stop observing test progress. Subclasses can override this method, but
they must invoke super implementation. */
- (void) stopObserving;
Шаг 2
2.а)
Создавая и регистрируя отдельный подкласс XCTestObserver
, мы не должны вмешиваться непосредственно в класс XCTestLog
по умолчанию.
Объявление константного ключа внутри XCTestObserver.h
предлагает только следующее:
/*! Setting the XCTestObserverClass user default to the name of a subclass of
XCTestObserver indicates that XCTest should use that subclass for reporting
test results rather than the default, XCTestLog. You can specify multiple
subclasses of XCTestObserver by specifying a comma between each one, for
example @"XCTestLog,FooObserver". */
XCT_EXPORT NSString * const XCTestObserverClassKey;
2б)
Несмотря на то, что обычная практика использования if(self == [ExampleAppDelegate class])
вокруг кода внутри +initialize
[Примечание: теперь он использует +load
], мне легче опустить его в этом конкретном случае: не нужно настраивать на правильную имя класса при копировании и вставке.
Кроме того, защита от запуска кода дважды не нужна здесь: это не включено в сборку релизов, и даже если мы подклассом ExampleAppDelegate
нет проблем при запуске этого кода более чем на один.
2.c)
В случае библиотек первый намек на проблему возник из этого комментария кода в Google Toolbox для Mac проект: GTMCodeCovereageApp.m
+ (void)load {
// Using defines and strings so that we don't have to link in XCTest here.
// Must set defaults here. If we set them in XCTest we are too late
// for the observer registration.
// (...)
И поскольку Ссылка на NSObject Class Reference указывает:
initialize. Инициализирует класс, прежде чем он получит свое первое сообщение.
load - вызывается всякий раз, когда класс или категория добавляются в среду Objective-C
Проект "EmptyLibrary"
Если кто-то пытается воспроизвести этот процесс, создав свой собственный проект "EmptyLibrary", помните, что вам нужно как-то вызвать код библиотеки из стандартных emtpy.
Если основной класс библиотеки не вызывается из тестов, компилятор попытается быть умным, и он не добавит его во время выполнения (поскольку он не вызван нигде), поэтому метод +load
получить вызов.
Вы можете просто вызвать какой-то безобидный метод (как Apple предлагает в своих Руководства по кодированию для Cocoa # Инициализация класса). Например:
- (void)testExample
{
[ExampleLibrary self];
}
Ответ 2
Поскольку вам нужно создать новый экземпляр XCTestSuiteRun в методе testSuiteDidStop, вы не получите правильные результаты при проверке ==. Вместо того, чтобы зависеть от равенства экземпляра, мы использовали простой счетчик и сбросили вызов, когда он достиг нулевого значения, что будет, когда заканчивается выполнение XCTestSuite верхнего уровня. Вероятно, есть более умные способы сделать это.
Во-первых, нам нужно было установить "Создавать файлы тестового покрытия = ДА" и "Инструментальный программный поток = ДА" в и цели тестирования и основного приложения.
#import <XCTest/XCTestLog.h>
#import <XCTest/XCTestSuiteRun.h>
#import <XCTest/XCTest.h>
// Workaround for XCode 5 bug where __gcov_flush is not called properly when Test Coverage flags are set
@interface GCovrTestObserver : XCTestLog
@end
#ifdef DEBUG
extern void __gcov_flush(void);
#endif
static NSUInteger sTestCounter = 0;
static id mainSuite = nil;
@implementation GCovrTestObserver
- (void)testSuiteDidStart:(XCTestRun *)testRun {
[super testSuiteDidStart:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
sTestCounter++;
if (mainSuite == nil) {
mainSuite = suite;
}
}
- (void)testSuiteDidStop:(XCTestRun *)testRun {
sTestCounter--;
[super testSuiteDidStop:testRun];
XCTestSuiteRun *suite = [[XCTestSuiteRun alloc] init];
[suite addTestRun:testRun];
if (sTestCounter == 0) {
__gcov_flush();
}
}
@end
Необходим дополнительный шаг, потому что вызов + инициализации не выполнялся на наблюдателе при включении в тестовый объект.
В AppDelegate добавьте следующее:
#ifdef DEBUG
+(void) initialize {
if([self class] == [AppDelegate class]) {
[[NSUserDefaults standardUserDefaults] setValue:@"GCovrTestObserver"
forKey:@"XCTestObserverClass"];
}
}
#endif
Ответ 3
Здесь другое решение, которое позволяет избежать необходимости редактировать AppDelegate
UIApplication + Instrumented.m(поместите это в свою основную цель):
@implementation UIApplication (Instrumented)
#ifdef DEBUG
+ (void)load
{
NSString* key = @"XCTestObserverClass";
NSString* observers = [[NSUserDefaults standardUserDefaults] stringForKey:key];
observers = [NSString stringWithFormat:@"%@,%@", observers, @"XCTCoverageFlusher"];
[[NSUserDefaults standardUserDefaults] setValue:observers forKey:key];
}
- (void)xtc_gcov_flush
{
extern void __gcov_flush(void);
__gcov_flush();
}
#endif
@end
XCTCoverageFlusher.m(поместите это в свою тестовую цель):
@interface XCTCoverageFlusher : XCTestObserver
@end
@implementation XCTCoverageFlusher
- (void) stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
SEL coverageFlusher = @selector(xtc_gcov_flush);
if ([application respondsToSelector:coverageFlusher])
{
objc_msgSend(application, coverageFlusher);
}
[application.delegate applicationWillTerminate:application];
}
@end
Ответ 4
- (void)applicationWillTerminate:(UIApplication*)application
должен быть определен в делетете приложения, а не в классе наблюдателя.
У меня не было проблем с библиотекой. "-lgov" не требуется, и вам не нужно добавлять какие-либо библиотеки. Охват поддерживается непосредственно компилятором LLVM.
Ответ 5
Процесс для этого немного отличается, если вы используете Specta, так как он делает свой собственный swizzling. Для меня работает следующее:
тестовый комплект:
@interface MyReporter : SPTNestedReporter // keeps the default reporter style
@end
@implementation MyReporter
- (void) stopObserving
{
[super stopObserving];
UIApplication* application = [UIApplication sharedApplication];
[application.delegate applicationWillTerminate:application];
}
@end
AppDelegate:
- (void)applicationWillTerminate:(UIApplication *)application
{
#ifdef DEBUG
extern void __gcov_flush(void);
__gcov_flush();
#endif
}
Затем вам нужно включить собственный пользовательский подкласс, установив переменную окружения SPECTA_REPORTER_CLASS
в MyReporter
в разделе "Выполнить" вашей основной схемы.
Ответ 6
GCOV Flush in - (void) applicationWillTerminate не работает для меня, я думаю, потому что мое приложение работает в фоновом режиме.
Я также установил 'Generate Test Coverage Files = YES' и 'Instrument Program Flow = YES', но не gcda-Files.
Затем я выполнил "__gcov_flush()" in - (void) tearDown из TestClass, который дал мне gcda-Files для моего TestClass;)
Затем я создал следующую функцию в приложении AppDelegate:
@interface AppDelegate : UIResponder <UIApplicationDelegate>
+(void)gcovFlush;
@end
@implementation AppDelegate
+(void)gcovFlush{
extern void __gcov_flush(void);
__gcov_flush();
NSLog(@"%s - GCOV FLUSH!", __PRETTY_FUNCTION__);
}
@end
Я назвал [AppDelegate gcovFlush] в моем - (void) tearDown и voilá, есть мои файлы gcda;)
Я надеюсь, что это поможет, пока Крис