使用Xcode5、iOS7模拟器和XCTest生成gcda文件

33

受到这个问题的解决方案的启发,我尝试使用相同的方法来进行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();
    }
}

我正在使用Xcode 5,Kiwi和一个外部脚本来读取.gcda文件,但仍然没有运气,因为没有生成.gcda文件.. :( - Vik
Xcode 5.1修复了这个问题并添加了llvm-gcov命令。 - ıɾuǝʞ
感谢 @kenji 的提醒。 https://developer.apple.com/library/ios/releasenotes/DeveloperTools/RN-Xcode/Introduction/Introduction.html - MdaG
6个回答

44

更新1:

经过阅读更多信息后,现在有两件事对我已经变得很清楚(重点加粗):

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

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

以及,

再说一遍:注入是复杂的。你应该明白的是:不要将你的app下的.m文件添加到测试目标中。你会得到意外的结果。

Testing View Controllers – #1 – Lighter View Controllers

下面的代码已经根据这两个洞见进行了修改...


更新2:

根据评论中@MdaG的请求,添加了有关如何使静态库工作的信息。对于库来说,主要更改的是:

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

  • 我们必须在+load方法中注册观察者,因为当从测试套件首次访问该类时(调用+initialize时),已经太晚让XCTest捕获它了。


解决方案

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

考虑以下两种情况之一:

  • 从头开始创建空应用程序的ExampleApp.xcodeproj
  • 作为独立的"Cocoa Touch Static Library"创建的ExampleLibrary.xcodeproj

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

  1. Create the GcovTestObserver.m file with the following code, inside the ExampleAppTests group:

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

    When doing a library, since there is no app to call, the flush can be invoked directly from the observer. In that case, add the file to the ExampleLibraryTests group with this code instead:

    #import <XCTest/XCTestObserver.h>
    
    @interface GcovTestObserver : XCTestObserver
    @end
    
    @implementation GcovTestObserver
    
    - (void)stopObserving
    {
        [super stopObserving];
        extern void __gcov_flush(void);
        __gcov_flush();
    }
    
    @end
    
  2. To register the test observer class, add the following code to the @implementation section of either one of:

    • ExampleAppDelegate.m file, inside the ExampleApp group
    • ExampleLibrary.m file, inside the ExampleLibrary group

     

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

    Previously, this answer suggested to use the +initialize method (and you can still do that in case of Apps) but it doesn't work for libraries…

    In the case of a library, the +initialize will probably be executed only when the tests invoke the library code for the first time, and by then it's already too late to register the observer. Using the +load method, the observer registration in always done in time, regardless of which scenario.

  3. In the case of Apps, add the following code to the @implementation section of the ExampleAppDelegate.m file, inside the ExampleApp group, to flush the coverage files on exiting the app:

    - (void)applicationWillTerminate:(UIApplication *)application
    {
    #ifdef DEBUG
        extern void __gcov_flush(void);
        __gcov_flush();
    #endif
    }
    
  4. Enable Generate Test Coverage Files and Instrument Program Flow by setting them to YES in the project build settings (for both the "Example" and "Example Tests" targets).

    To do this in an easy and consistent way, I've added a Debug.xcconfig file associated with the project's "Debug" configuration, with the following declarations:

    GCC_GENERATE_TEST_COVERAGE_FILES = YES
    GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
    
  5. Make sure all the project's .m files are also included in the "Compile Sources" build phase of the "Example Tests" target. Don't do this: app code belongs to the app target, test code belongs to the test target!

运行您项目的测试后,您将在此处找到生成的覆盖率文件: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

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 [注:现在使用+load]中的代码周围使用if(self == [ExampleAppDelegate class])是常见的做法,但在这种特殊情况下省略它会更容易:复制和粘贴时不需要调整正确的类名。

此外,在此处不需要保护代码运行两次:这不包括在发布版本中,即使我们子类化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”项目来复制此过程,请记住您需要以某种方式从默认的测试中调用库代码。

如果主库类未从测试中调用,编译器将尝试变得聪明并且不会将其添加到运行时(因为它没有被任何地方调用),因此+load方法不会被调用。

您可以简单地调用一些无害的方法(正如Apple在他们的Cocoa编码指南#类初始化中建议的那样)。例如:

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

3
对我没用:由于应用程序委托中的__gcov_flush(),我遇到了链接错误。我尝试将-lgcov标志添加到“其他链接器标志”,但也没有帮助。有什么想法吗? - aspyct
它对我有效。但是需要注意的是,“XCTestLog”标志不是必需的,并且会在控制台中提供重复日志。 - KamilPyc
2
@KamilPyc 奇怪...如果我从用户默认设置中删除“XCTestLog”类,就不会得到任何日志。也许你的“GcovTestObserver”像其他答案和问题中所做的那样是“XCTestLog”的子类?这可以解释重复的日志。为了使答案的代码工作,您需要将“XCTestObserver”子类化。 - Hugo Ferreira
1
@KamilPyc 这并不是实际上的“好处”(两种解决方案都能很好地完成工作),而更多的是一种更清晰的面向对象的方法: “XCTestLog”已经是“XCTestObserver”的子类(即它是一个专门的测试观察器,其工作是记录它们)。通过子类化“XCTestLog”,您将表示您想要创建一个专门的测试记录器,其工作是记录某些内容或以不同的方式记录。通过子类化“XCTestObserver”,您表示您只想要一种不同类型的专门的测试观察器,其工作是在测试结束时刷新覆盖文件。 - Hugo Ferreira
1
@MdaG 谢谢您的提问!我做了一些研究,但基本上是相同的概念,尽管刷新可以直接在“-stopObserving”中完成,我们必须使用“+load”而不是“+initialize”来注册观察者以便XCTest及时捕获它。我已经更新了答案,包含这些信息。 - Hugo Ferreira
显示剩余13条评论

3

由于你必须在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

谢谢,但它不起作用。首先,我必须在测试目标中将“仪器程序流”激活为YES,以便使__gcov_flush()编译。现在__gcov_flush可以工作,但只能在GCovTestObserver中调用,而不能在AppDelegate中调用。因此,我有了gcda,但仅适用于测试目标(具有讽刺意味的是,甚至包括GCovTestObserver.gcda)。 - Francescu
感谢反馈!是的,就像上面的例子一样,我已经将 __gcov_flush 调用从 AppDelegate 移到了 testSuiteDidStop: 方法中。可能还有一些细节需要说明。我们不得不在测试和主目标中都设置 'Generate Test Coverage Files=YES' 和 'Instrument Program Flow=YES' 才能使其正常工作。但是我已经覆盖了所有源文件和测试文件。我会想想还有什么其他有用的信息。同时,我已经编辑了答案以包含这部分内容。 - sanderson
你是如何处理需要将XCTest.framework添加到主目标中的事实?对我来说,这根本不起作用: ld: building for iOS Simulator, but linking against dylib built for MacOSX file '/Applications/Xcode.app/Contents/Developer/Library/Frameworks/XCTest.framework/XCTest' for architecture i386 clang: error: linker command failed with exit code 1 (use -v to see invocation) - Beat Rupp
1
如果您直接使用字符串@"XCTestObserverClass"而不是XCTestObserverClassKey,那么在主目标中就不需要包含XCTest.framework。当然,这一切都建立在这样一个假设的前提下,即有一天他们会修复这个问题或者向我们展示为什么他们将其删除。他们是否试图将此功能移入尚未真正工作的新CI服务器中? - sanderson
谢谢,我现在已经成功解决了问题,但是我不确定应该接受哪个答案,因为目前为止每个人都做出了贡献。我注意到我需要将非测试文件添加到测试目标的构建阶段,以便生成gcda文件。 - MdaG

1
我是一个有用的助手,可以为您翻译文本。
这里有另一种解决方案,避免了编辑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

补充一下,Google Toolbox for Mac (GTM) 有类似的实现。我在这个链接中找到了步骤:http://qualitycoding.org/ios-7-code-coverage/ - Chris

0

- (void)applicationWillTerminate:(UIApplication*)application 必须在您的应用程序委托中定义,而不是在观察者类中定义。

我没有遇到任何库问题。不需要 "-lgov",也不必添加任何库。覆盖率直接由LLVM编译器支持。


谢谢,那很有道理。如果所有内容都包含在LLVM中,为什么我会收到这个错误?i386架构未定义符号: “___gcov_flush”,引用自: AppDelegate.o中的-[AppDelegate applicationWillTerminate:] ld:i386架构找不到符号 clang:链接器命令失败,退出代码为1(使用-v查看调用) - MdaG
你必须为正确的目标设置“生成测试覆盖文件”。 - Sulthan
啊好的,那我需要一个属于它自己的测试方案......我编译并运行了测试,但仍然没有 gcda 文件。 :-( - MdaG
我还需要访问 gcd 文件。 - StackRunner

0

如果你使用的是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_CLASSMyReporter来启用您的自定义报告生成器子类。


0

GCOV在-(void)applicationWillTerminate中的Flush对我无效,可能是因为我的App正在后台运行。

我还设置了“Generate Test Coverage Files=YES”和“Instrument Program Flow=YES”,但没有生成gcda文件。

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

然后我在我的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文件就出现了;)

希望这能帮到你,再见 Chris


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接