33

Being inspired by the solution to this question I tried using the same approach with XCTest.

I've set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES'.

XCode still doesn't produce any gcda files. Anyone have any ideas of how to solve this?

Code:

#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

In AppDelegate.m I have:

extern void __gcov_flush(void);
- (void)applicationWillTerminate:(UIApplication *)application {
    __gcov_flush();
}

EDIT: I edited the question to reflect the current status (without the red herrings).

EDIT To make it work I had to add the all the files under test to the test target including 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();
    }
}
Community
  • 1
  • 1
MdaG
  • 2,680
  • 2
  • 34
  • 46
  • I'm using Xcode 5, Kiwi and an external script for reading .gcda files but still no luck because no .gcda file is generated.. :( – Vik Oct 02 '13 at 13:15
  • Xcode 5.1 fix this issue and add llvm-gcov command – ıɾuǝʞ Mar 12 '14 at 08:28
  • Thanks for the heads up @kenji https://developer.apple.com/library/ios/releasenotes/DeveloperTools/RN-Xcode/Introduction/Introduction.html – MdaG Mar 13 '14 at 08:07

6 Answers6

44

Update 1:

After reading a bit more about this, 2 things have now become clear to me (emphasis added):

Tests and the tested application are compiled separately. Tests are actually injected into the running application, so the __gcov_flush() must be called inside the application not inside the tests.

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

and,

Again: Injection is complex. Your take away should be: Don’t add .m files from your app to your test target. You’ll get unexpected behavior.

Testing View Controllers – #1 – Lighter View Controllers

The code below was changed to reflect these two insights…


Update 2:

Added information on how to make this work for static libraries, as requested by @MdaG in the comments. The main changes for libraries is that:

  • We can flush directly from the -stopObserving method because there isn't a separate app where to inject the tests.

  • We must register the observer in the +load method because by the time the +initialize is called (when the class is first accessed from the test suite) it's already too late for XCTest to pick it up.


Solution

The other answers here have helped me immensely in setting up code coverage in my project. While exploring them, I believe I've managed to simplify the code for the fix quite a bit.

Considering either one of:

  • ExampleApp.xcodeproj created from scratch as an "Empty Application"
  • ExampleLibrary.xcodeproj created as an independent "Cocoa Touch Static Library"

These were the steps I took to enable Code Coverage generation in 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!

After running the tests for your project, you'l be able to find the generated coverage files for the Example.xcodeproj in here:

cd ~/Library/Developer/Xcode/DerivedData/
find ./Example-* -name *.gcda

Notes

Step 1

The method declaration inside XCTestObserver.h indicates:

/*! 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;

Step 2

2.a)

By creating and registering a separate XCTestObserver subclass, we avoid having to interfere directly with the default XCTestLog class.

The constant key declaration inside XCTestObserver.h suggests just that:

/*! 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)

Even though it's common practice to use if(self == [ExampleAppDelegate class]) around the code inside +initialize [Note: it's now using +load], I find it easier to omit it in this particular case: no need to adjust to the correct class name when doing copy & paste.

Also, the protection against running the code twice isn't really necessary here: this is not included in the release builds, and even if we subclass ExampleAppDelegate there is no problem in running this code more than one.

2.c)

In the case of libraries, the first hint of the problem came from this code comment in the Google Toolbox for Mac project: 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.
  // (...)

And as the NSObject Class Reference indicates:

initialize — Initializes the class before it receives its first message

load — Invoked whenever a class or category is added to the Objective-C runtime

The “EmptyLibrary” project

In case someone tries to replicate this process by creating their own "EmptyLibrary" project, please bear in mind that you need to invoke the library code from the default emtpy tests somehow.

If the main library class is not invoked from the tests, the compiler will try to be smart and it won't add it to the runtime (since it's not being called anywhere), so the +load method doesn't get called.

You can simply invoke some harmless method (as Apple suggests in their Coding Guidelines for Cocoa # Class Initialization). For example:

- (void)testExample
{
    [ExampleLibrary self];
}
Community
  • 1
  • 1
Hugo Ferreira
  • 1,584
  • 16
  • 17
  • This should be the accepted answer, it's the only one that worked for me. Thanks for taking the time to investigate this! – Adam Sharp Oct 24 '13 at 06:36
  • 3
    Doesn't work for me: I have a linkage error due to the __gcov_flush() in the app delegate. I tried to add the -lgcov flag to the "other linker flags", but it didn't help neither. Any idea? – aspyct Oct 25 '13 at 13:41
  • @Antoine_935 I suspect you're missing the GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES build setting. If you prefer, you can also do it manually in Xcode by going to the project's "Build Settings" and switching the "Instrument Program Flow" to "Yes" for the "Debug" configuration. Don't forget to also do the same for the "Generate Test Coverage Files" or it will link but won't generate the .gcda files. – Hugo Ferreira Oct 26 '13 at 09:13
  • @Hugo not so :( I have the flags set to YES for both targets (app and tests), but still impossible to compile. Any more idea? – aspyct Oct 28 '13 at 07:42
  • @Antoine_935 Does it also happen when you create a new project from scratch? Can you gist your full linkage error somewhere? – Hugo Ferreira Oct 29 '13 at 08:44
  • @Hugo Actually I just updated to Xcode 5.0.1 and the coverage files are now generated without any call to __gcov_flush(). So everything's cool :) Thanks for your support ! – aspyct Oct 29 '13 at 09:23
  • It work for me. But one remark, flag "XCTestLog" is not necessary and it provide duplicate log in console. – KamilPyc Nov 06 '13 at 10:46
  • 2
    @KamilPyc strange… if I remove the class "XCTestLog" from the user defaults I get no logs at all. Maybe your "GcovTestObserver" is subclassing "XCTestLog" like it was done in the other answers and in the question? That would explain the duplicate logs. For *this* answer's code to work you need to subclass "XCTestObserver" instead. – Hugo Ferreira Nov 06 '13 at 15:19
  • Yes you are correct - I messed up those two answers. But what the benefist from subclassing "XCTestObserver"? – KamilPyc Nov 06 '13 at 15:23
  • 1
    @KamilPyc it's not so much of a practical "benefit" (both solutions do the job just fine) but it's more of a cleaner OO approach: "XCTestLog" is already a subclass of "XCTestObserver" (i.e. it's a specialised *observer of tests* whose job is to log them). By subclassing "XCTestLog" you'd be saying that you want to create a specialised *logger of tests* whose is to log something more or differently. By subclassing "XCTestObserver" you're saying that you just want a different kind of specialised *observer of tests*, one whose job is to flush the coverage files when the tests end. – Hugo Ferreira Nov 06 '13 at 19:29
  • Great solution, thanks! I created the original SO question that you referenced, and I thought we had it solved. Only for static libs though. So I just lost a few more hours grappling with this again yesterday. – Jasper Blues Jan 11 '14 at 05:23
  • How would you do this if your project is a standalone library with no Application? – MdaG Jan 14 '14 at 15:41
  • 1
    @MdaG thanks for asking! had to do a bit of research but it's pretty much the same concept, although the flush can be done directly in the `-stopObserving` and we must use `+load` instead of `+initialize` to register the observer in time for XCTest to pick it up. I've updated the answer with this information. – Hugo Ferreira Jan 15 '14 at 16:13
  • thanks @JasperBlues, i just collected all the nuggets together :) that thread you created had lots priceless information. i've also updated this answer to cope with static libs (had an issue with using the `+initialize`)… maybe you could take a look/review? thx – Hugo Ferreira Jan 15 '14 at 16:23
  • I'm having a bit of a problem with this solution - I'm creating a library, but I don't have a single `ExampleLibrary.m` file - just a header than imports the headers of the other classes in my library. Any suggestions of where to stick my `load` method? – James Frost Feb 05 '14 at 15:13
  • I've got a unit test suite testing models inside of my app. After hours of struggling, I noticed that invoking `__gcov_flush()` from within the App Delegate **does not** generate proper coverage. Instead, I had to invoke it in `-testSuiteDidStop:notification`, to get it working. Hope someone else doesn't have to spend hours finding this out... – Kristofer Sommestad Feb 06 '14 at 07:32
  • If anyone else has problems with __gcov_flush() being undefined, make sure set the `Generate Test Coverage Files` and `Instrument Program Flow` settings are both set to `YES`. – Brian Gerstle Feb 24 '14 at 22:08
  • Nice solution, but `XCTestObserver` is deprecated now. – HiveHicks Sep 19 '14 at 14:11
3

Because you have to create a new XCTestSuiteRun instance in the testSuiteDidStop method, you are not going to get the proper results on an == check. Instead of depending on instance equality, we used a simple counter and call flush when it hits zero, which it will when the top-level XCTestSuite finishes executing. There are probably more clever ways to do this.

First, we had to set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES' in both the Test and main app targets.

#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

There was an additional step required, because the +initialize call was not being made on the observer when included in the Test target.

In the AppDelegate, add the following:

#ifdef DEBUG
+(void) initialize {
    if([self class] == [AppDelegate class]) {
        [[NSUserDefaults standardUserDefaults] setValue:@"GCovrTestObserver"
                                                 forKey:@"XCTestObserverClass"];
    }
}
#endif
sanderson
  • 31
  • 2
  • Thx but it doesn't work. First I had to activate "Instrument program flow" to YES even in the Tests target in order to make the __gcov_flush() to compile. Now the __gcov_flush works but only called in the GCovTestObserver not in AppDelegate. So I have gcda but only for tests Target (and, ironically even the GCovTestObserver.gcda). – Francescu Oct 03 '13 at 14:15
  • Thanks for the feedback! Yes, as in the example above, I've gone ahead and moved the __gcov_flush call out of the AppDelegate and into the testSuiteDidStop: method directly. A few more details might be helpful. We had to set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES' in BOTH the Test and main Targets to get this to work. But I'm getting coverage on all of the source and test files. I'll try and think of anything else we did that might be helpful. Meanwhile, I've edited the answer to include that part. – sanderson Oct 03 '13 at 20:03
  • How are you dealing with the fact that you need to add the XCTest.framework to your main target? For me this does not work at all: 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 Oct 04 '13 at 15:37
  • 1
    Don't need to include XCTest.framework in the main target if you use the string @"XCTestObserverClass" directly instead of XCTestObserverClassKey. This is all being done, of course, under the assumption that some magical day is coming where they (a) fix this or (b) show us why they removed it. Are they trying to move this functionality into the new CI Server that doesn't really work yet? – sanderson Oct 04 '13 at 18:03
  • Thanks, I've managed to get it to work now, but I'm not sure which answer to accept as so far everyone has contributed. I did notice that I needed to add non test files to the build phases of the test target for the gcda files to spawn. – MdaG Oct 08 '13 at 13:07
1

Here's another solution that avoids having to edit your AppDelegate

UIApplication+Instrumented.m (put this in your main target):

@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 (put this in your test target):

@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
Jasper Blues
  • 28,258
  • 22
  • 102
  • 185
  • Just to add to this, Google Toolbox for Mac (GTM) has a similar implementation. I found the steps at this link: http://qualitycoding.org/ios-7-code-coverage/ – Chris Mar 13 '14 at 17:20
0

- (void)applicationWillTerminate:(UIApplication*)application must be defined in your application delegate, not in the observer class.

I didn't have any library problems. "-lgov" is not needed and you don't have to add any libraries. Coverage is supported directly by LLVM compiler.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Thanks, that makes sense. If it's all included in LLVM, why do I get this error? Undefined symbols for architecture i386: "___gcov_flush", referenced from: -[AppDelegate applicationWillTerminate:] in AppDelegate.o ld: symbol(s) not found for architecture i386 clang: error: linker command failed with exit code 1 (use -v to see invocation) – MdaG Oct 02 '13 at 15:04
  • You have to set up "Generate Test Coverage Files" for the correct target. – Sulthan Oct 02 '13 at 15:07
  • Ah ok, then I need a scheme of its' own for tests... I compiled and ran the tests, but still no gcda files. :-( – MdaG Oct 02 '13 at 15:24
  • I also need access to gcd a files – StackRunner Oct 09 '13 at 16:44
0

The process for this is a little different if you're using Specta, since it does its own swizzling. The following is working for me:

Test Bundle:

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

You then need to enable your custom reporter subclass by setting the environmental variable SPECTA_REPORTER_CLASS to MyReporter in the Run section of your main scheme.

Jonathan Crooke
  • 912
  • 7
  • 19
0

GCOV Flush in -(void)applicationWillTerminate didn't work for me, I think because my App is running in Background.

I also set 'Generate Test Coverage Files=YES' and 'Instrument Program Flow=YES' but no gcda-Files.

Then I executed "__gcov_flush()" in -(void)tearDown from the TestClass, which gave me gcda-Files for my TestClass ;)

Then I created the following Function in my 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

I called [AppDelegate gcovFlush] in my -(void)tearDown and voilá, there are my gcda Files ;)

I hope this helps, bye Chris

sPooKee
  • 131
  • 1
  • 3