【问题标题】:Generate gcda-files with Xcode5, iOS7 simulator and XCTest使用 Xcode5、iOS7 模拟器和 XCTest 生成 gcda 文件
【发布时间】:2013-10-08 19:52:00
【问题描述】:

受到solution to this question 的启发,我尝试使用与 XCTest 相同的方法。

我已设置“生成测试覆盖率文件=YES”和“仪器程序流程=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();
}

编辑:我编辑了问题以反映当前状态(没有红鲱鱼)。

编辑为了使它工作,我必须将所有正在测试的文件添加到测试目标,包括 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();
    }
}

【问题讨论】:

标签: ios7 code-coverage xcode5


【解决方案1】:

更新 1:

在阅读了更多关于此的内容后,我现在明白了两件事(强调添加):

测试和被测试的应用程序是分开编译的。测试实际上是注入到正在运行的应用程序中,所以 __gcov_flush() 必须在应用程序内部而不是在测试内部调用

——Xcode5 Code Coverage (from cmd-line for CI builds) - Stack Overflow

和,

再次重申:注射很复杂。您的收获应该是:不要将 .m 文件从您的应用添加到您的测试目标中。您会得到意想不到的行为。

——Testing View Controllers – #1 – Lighter View Controllers

以下代码已更改以反映这两个见解……


更新 2:

根据 cmets 中 @MdaG 的要求,添加了有关如何使静态库工作的信息。库的主要变化是:

  • 我们可以直接从 -stopObserving 方法刷新,因为没有单独的应用程序可以在其中注入测试。

  • 我们必须在+load 方法中注册观察者,因为当+initialize 被调用时(当第一次从测试套件访问该类时),XCTest 接收它已经太迟了。


解决方案

这里的其他答案极大地帮助了我在项目中设置代码覆盖率。在探索它们时,我相信我已经成功地简化了修复代码。

考虑以下之一:

  • ExampleApp.xcodeproj 从头开始​​创建为“空应用程序”
  • ExampleLibrary.xcodeproj 创建为独立的“Cocoa Touch 静态库”

这些是我在 Xcode 5 中启用代码覆盖生成的步骤:

  1. ExampleAppTests 组内使用以下代码创建 GcovTestObserver.m 文件:

    #import <XCTest/XCTestObserver.h>
    
    @interface GcovTestObserver : XCTestObserver
    @end
    
    @implementation GcovTestObserver
    
    - (void)stopObserving
    {
        [super stopObserving];
        UIApplication* application = [UIApplication sharedApplication];
        [application.delegate applicationWillTerminate:application];
    }
    
    @end
    

    在做库的时候,因为没有app可以调用,flush可以直接从observer调用。在这种情况下,请使用以下代码将文件添加到 ExampleLibraryTests 组:

    #import <XCTest/XCTestObserver.h>
    
    @interface GcovTestObserver : XCTestObserver
    @end
    
    @implementation GcovTestObserver
    
    - (void)stopObserving
    {
        [super stopObserving];
        extern void __gcov_flush(void);
        __gcov_flush();
    }
    
    @end
    
  2. 要注册测试观察者类,请将以下代码添加到其中之一的@implementation 部分:

    • ExampleAppDelegate.m 文件,在 ExampleApp 组内
    • ExampleLibrary.m 文件,在 ExampleLibrary 组内

     

    #ifdef DEBUG
    + (void)load {
        [[NSUserDefaults standardUserDefaults] setValue:@"XCTestLog,GcovTestObserver"
                                                 forKey:@"XCTestObserverClass"];
    }
    #endif
    

    以前,此答案建议使用 +initialize 方法(对于应用程序,您仍然可以这样做),但它不适用于库......

    在库的情况下,+initialize 可能只会在测试第一次调用库代码时执行,到那时注册观察者已经太晚了。使用+load 方法,无论哪种情况,观察者注册总是及时完成。

  3. 对于应用程序,将以下代码添加到 ExampleApp 组内的ExampleAppDelegate.m 文件的@implementation 部分,以在退出应用程序时刷新覆盖文件:

    - (void)applicationWillTerminate:(UIApplication *)application
    {
    #ifdef DEBUG
        extern void __gcov_flush(void);
        __gcov_flush();
    #endif
    }
    
  4. 通过在项目构建设置中将Generate Test Coverage FilesInstrument Program Flow 设置为YES 来启用它们(对于“示例”和“示例测试”目标)。

    为了以简单且一致的方式执行此操作,我添加了一个 Debug.xcconfig 文件 associated with the project's "Debug" configuration,并带有以下声明:

    GCC_GENERATE_TEST_COVERAGE_FILES = YES
    GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
    
  5. 确保所有项目的.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's time 
    to stop observing test progress. Subclasses can override this method, but 
    they must invoke super's implementation. */
- (void) stopObserving;

第二步

2.a)

通过创建和注册一个单独的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.b)

尽管在+initialize 内的代码周围使用if(self == [ExampleAppDelegate class]) 是一种常见做法[注意:现在使用+load],我发现在这种特殊情况下省略它更容易:复制粘贴时无需调整到正确的类名。

此外,这里实际上并不需要防止运行代码两次:这不包含在发布版本中,即使我们将ExampleAppDelegate 子类化,多次运行此代码也没有问题。

2.c)

在库的情况下,问题的第一个提示来自Google Toolbox for 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 在他们的 Coding Guidelines for Cocoa # Class Initialization 中建议的那样)。例如:

- (void)testExample
{
    [ExampleLibrary self];
}

【讨论】:

  • 这应该是公认的答案,它是唯一对我有用的答案。感谢您抽出宝贵时间对此进行调查!
  • 对我不起作用:由于应用程序委托中的 __gcov_flush() 导致链接错误。我试图将 -lgcov 标志添加到“其他链接器标志”,但它也没有帮助。有什么想法吗?
  • @KamilPyc 奇怪......如果我从用户默认值中删除类“XCTestLog”,我根本不会得到任何日志。也许您的“GcovTestObserver”是“XCTestLog”的子类,就像在其他答案和问题中所做的那样?这将解释重复的日志。要使 this 答案的代码正常工作,您需要将“XCTestObserver”子类化。
  • @KamilPyc 这不是一个实用的“好处”(两种解决方案都可以很好地完成工作),但它更像是一种更清洁的 OO 方法:“XCTestLog”已经是“XCTestObserver”的子类(即它是一个专门的测试观察者,其工作是记录它们)。通过将“XCTestLog”子类化,您会说您想要创建一个专门的 测试记录器 来记录更多或不同的内容。通过子类化“XCTestObserver”,您是说您只需要一种不同类型的专用测试观察者,其工作是在测试结束时刷新覆盖文件。
  • @MdaG 感谢您的提问!必须做一些研究,但它的概念几乎相同,虽然刷新可以直接在-stopObserving 中完成,我们必须使用+load 而不是+initialize 及时注册观察者,以便 XCTest 选择它向上。我已经用这些信息更新了答案。
【解决方案2】:

因为您必须在 testSuiteDidStop 方法中创建一个新的 XCTestSuiteRun 实例,所以您不会在 == 检查中获得正确的结果。我们不依赖于实例相等性,而是使用一个简单的计数器并在它达到零时调用flush,它会在顶级XCTestSuite 完成执行时调用。可能有更聪明的方法可以做到这一点。

首先,我们必须在 测试和主要应用目标中设置“生成测试覆盖率文件=YES”和“仪器程序流程=YES”。

#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

需要一个额外的步骤,因为当包含在测试目标中时,没有对观察者进行 +initialize 调用。

在 AppDelegate 中,添加以下内容:

#ifdef DEBUG
+(void) initialize {
    if([self class] == [AppDelegate class]) {
        [[NSUserDefaults standardUserDefaults] setValue:@"GCovrTestObserver"
                                                 forKey:@"XCTestObserverClass"];
    }
}
#endif

【讨论】:

  • 谢谢,但它不起作用。首先,即使在测试目标中,我也必须将“仪器程序流程”激活为“是”,以使 __gcov_flush() 能够编译。现在 __gcov_flush 可以工作,但只在 GCovTestObserver 中调用,而不是在 AppDelegate 中调用。所以我有 gcda 但只用于测试目标(讽刺的是,甚至是 GCovTestObserver.gcda)。
  • 感谢您的反馈!是的,就像上面的示例一样,我已经将 __gcov_flush 调用移出 AppDelegate 并直接移入 testSuiteDidStop: 方法。更多细节可能会有所帮助。我们必须在测试和主要目标中设置“生成测试覆盖文件=YES”和“仪器程序流程=YES”才能使其正常工作。但是我得到了所有源文件和测试文件的报道。我会尝试想想我们所做的任何其他可能有帮助的事情。同时,我已经编辑了答案以包含该部分。
  • 您如何处理需要将 XCTest.framework 添加到主要目标的事实?对我来说,这根本不起作用:ld:为 iOS 模拟器构建,但链接到为 MacOSX 文件构建的 dylib '/Applications/Xcode.app/Contents/Developer/Library/Frameworks/XCTest.framework/XCTest' 架构 i386 clang : 错误:链接器命令失败,退出代码为 1(使用 -v 查看调用)
  • 如果直接使用字符串@"XCTestObserverClass"而不是XCTestObserverClassKey,则不需要在主目标中包含XCTest.framework。当然,这一切都是在假设某个神奇的日子即将到来的情况下完成的,他们 (a) 解决了这个问题,或者 (b) 向我们展示了他们为什么删除它。他们是否试图将此功能转移到尚未真正起作用的新 CI 服务器中?
  • 谢谢,我现在已经设法让它工作,但我不确定到目前为止每个人都贡献了哪个答案。我确实注意到我需要将非测试文件添加到测试目标的构建阶段以生成 gcda 文件。
【解决方案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 编译器直接支持覆盖。

【讨论】:

  • 谢谢,这是有道理的。如果它全部包含在 LLVM 中,为什么会出现此错误?体系结构 i386 的未定义符号:“___gcov_flush”,引用自:AppDelegate.o ld 中的 -[AppDelegate applicationWillTerminate:]:未找到体系结构 i386 的符号:错误:链接器命令失败,退出代码为 1(使用 -v 到见调用)
  • 您必须为正确的目标设置“生成测试覆盖文件”。
  • 啊,好吧,那么我需要一个自己的测试方案......我编译并运行了测试,但仍然没有 gcda 文件。 :-(
  • 我还需要访问 gcd a 文件
【解决方案5】:

如果您使用的是 Specta,则此过程会有所不同,因为它会自行调整。以下对我有用:

测试包:

@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。

    然后我在 TestClass 的 -(void)tearDown 中执行了“__gcov_flush()”,这为我的 TestClass 提供了 gcda-Files ;)

    然后我在我的 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
    

    我在我的 -(void)tearDown 中调用了 [AppDelegate gcovFlush],瞧,有我的 gcda 文件 ;)

    我希望这会有所帮助,再见,克里斯

    【讨论】:

      猜你喜欢
      • 2011-06-08
      • 2012-04-18
      • 2014-07-18
      • 2013-10-11
      • 2014-03-30
      • 2016-08-25
      • 2014-02-14
      • 2014-10-15
      相关资源
      最近更新 更多