web-dev-qa-db-fra.com

Générez des fichiers gcda avec Xcode5, simulateur iOS7 et XCTest

S'inspirant de la solution à cette question J'ai essayé d'utiliser la même approche avec XCTest.

J'ai défini "Générer des fichiers de couverture de test = OUI" et "Flux de programme d'instrument = OUI".

XCode ne produit toujours aucun fichier gcda. Quelqu'un a des idées sur la façon de résoudre ce problème?

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

Dans AppDelegate.m, j'ai:

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

[~ # ~] modifier [~ # ~] : J'ai modifié la question pour refléter l'état actuel (sans les harengs rouges).

[~ # ~] modifier [~ # ~] Pour que cela fonctionne, j'ai dû ajouter tous les fichiers testés à la cible de test, y compris 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();
    }
}
33
MdaG

Mise à jour 1:

Après avoir lu un peu plus à ce sujet, 2 choses sont maintenant devenues claires pour moi (je souligne):

Les tests et l'application testée sont compilés séparément. Les tests sont en fait injectés dans l'application en cours d'exécution, donc la __gcov_flush() doit être appelée à l'intérieur de l'application et non à l'intérieur des tests .

- Couverture du code Xcode5 (à partir de la ligne de commande pour les builds CI) - Débordement de pile

et,

Encore une fois: l'injection est complexe. Vos plats à emporter devraient être: N'ajoutez pas de fichiers .m de votre application à votre cible de test. Vous obtiendrez un comportement inattendu.

- Test des contrôleurs de vue - # 1 - Contrôleurs de vue plus légers

Le code ci-dessous a été modifié pour refléter ces deux idées…


Mise à jour 2:

Ajout d'informations sur la façon de faire fonctionner cela pour les bibliothèques statiques, comme demandé par @ MdaG dans les commentaires. Les principaux changements pour les bibliothèques sont les suivants:

  • Nous pouvons vider directement à partir de la méthode -stopObserving car il n'y a pas d'application séparée où injecter les tests.

  • Nous devons enregistrer l'observateur dans la méthode +load car au moment où le +initialize est appelé (lorsque la classe est accédée pour la première fois à partir de la suite de tests), il est déjà trop tard pour que XCTest la récupère.


Solution

Les autres réponses ici m'ont énormément aidé à configurer la couverture du code dans mon projet. En les explorant, je pense avoir réussi à simplifier un peu le code du correctif.

Considérant l'un ou l'autre de:

  • ExampleApp.xcodeproj créé à partir de zéro en tant qu '"application vide"
  • ExampleLibrary.xcodeproj créé en tant que "bibliothèque statique Cocoa Touch"

Voici les étapes que j'ai suivies pour activer la génération de couverture de code dans Xcode 5:

  1. Créez le fichier GcovTestObserver.m avec le code suivant, à l'intérieur du groupe ExampleAppTests :

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

    Lors de la création d'une bibliothèque, puisqu'il n'y a pas d'application à appeler, le vidage peut être appelé directement depuis l'observateur. Dans ce cas, ajoutez le fichier au groupe ExampleLibraryTests avec ce code à la place:

    #import <XCTest/XCTestObserver.h>
    
    @interface GcovTestObserver : XCTestObserver
    @end
    
    @implementation GcovTestObserver
    
    - (void)stopObserving
    {
        [super stopObserving];
        extern void __gcov_flush(void);
        __gcov_flush();
    }
    
    @end
    
  2. Pour enregistrer la classe d'observateur de test, ajoutez le code suivant à la section @implementation de l'un des deux:

    • Fichier ExampleAppDelegate.m, à l'intérieur du groupe ExampleApp
    • Fichier ExampleLibrary.m, à l'intérieur du groupe ExampleLibrary

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

    Auparavant, cette réponse suggérait d'utiliser la méthode +initialize (et vous pouvez toujours le faire dans le cas des applications) mais cela ne fonctionne pas pour les bibliothèques…

    Dans le cas d'une bibliothèque, le +initialize ne sera probablement exécuté que lorsque les tests invoqueront le code de la bibliothèque pour la première fois, et d'ici là, il est déjà trop tard pour enregistrer l'observateur. En utilisant la méthode +load, l'enregistrement de l'observateur s'effectue toujours à temps, quel que soit le scénario.

  3. Dans le cas des applications, ajoutez le code suivant à la section @implementation du fichier ExampleAppDelegate.m, à l'intérieur du groupe ExampleApp , pour vider les fichiers de couverture à la sortie de l'application:

    - (void)applicationWillTerminate:(UIApplication *)application
    {
    #ifdef DEBUG
        extern void __gcov_flush(void);
        __gcov_flush();
    #endif
    }
    
  4. Activez Generate Test Coverage Files et Instrument Program Flow en les définissant sur YES dans les paramètres de construction du projet (pour les cibles "Exemple" et "Exemple de tests").

    Pour ce faire de manière simple et cohérente, j'ai ajouté un fichier Debug.xcconfigassocié à la configuration "Debug" du projet , avec les déclarations suivantes:

    GCC_GENERATE_TEST_COVERAGE_FILES = YES
    GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
    
  5. Assurez-vous que tous les fichiers .m du projet sont également inclus dans la phase de compilation "Compiler les sources" de la cible "Exemples de tests". Ne faites pas cela: le code d'application appartient à la cible de l'application, le code de test appartient à la cible de test !

Après avoir exécuté les tests de votre projet, vous pourrez trouver les fichiers de couverture générés pour le Example.xcodeproj ici:

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

Remarques

Étape 1

La déclaration de méthode à l'intérieur de XCTestObserver.h indique:

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

Étape 2

2.a)

En créant et en enregistrant une sous-classe XCTestObserver distincte, nous évitons d'avoir à interférer directement avec la classe XCTestLog par défaut.

La déclaration de clé constante dans XCTestObserver.h suggère simplement que:

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

Même s'il est courant d'utiliser if(self == [ExampleAppDelegate class]) autour du code à l'intérieur de +initialize [Remarque: il utilise maintenant +load], je trouve plus facile de l'omettre dans ce cas particulier: non besoin de s'adapter au nom de classe correct lors du copier-coller.

De plus, la protection contre l'exécution du code deux fois n'est pas vraiment nécessaire ici: cela n'est pas inclus dans les versions, et même si nous sous-classons ExampleAppDelegate, il n'y a aucun problème à exécuter ce code plus d'un.

2.c)

Dans le cas des bibliothèques, le premier indice du problème est venu de ce commentaire de code dans le projet 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.
  // (...)

Et comme l'indique NSObject Class Reference :

initialize - Initialise la classe avant qu'elle ne reçoive son premier message

load - Appelé chaque fois qu'une classe ou une catégorie est ajoutée au runtime Objective-C

Le projet "EmptyLibrary"

Dans le cas où quelqu'un essaie de reproduire ce processus en créant son propre projet "EmptyLibrary", gardez à l'esprit que vous devez invoquer le code de bibliothèque à partir des tests par défaut emtpy d'une manière ou d'une autre.

Si la classe de bibliothèque principale n'est pas invoquée à partir des tests, le compilateur essaiera d'être intelligent et ne l'ajoutera pas au runtime (car il n'est appelé nulle part), donc le +load La méthode n'est pas appelée.

Vous pouvez simplement invoquer une méthode inoffensive (comme Apple suggère dans leur Coding Guidelines for Cocoa # Class Initialization ). Par exemple:

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

Étant donné que vous devez créer une nouvelle instance XCTestSuiteRun dans la méthode testSuiteDidStop, vous n'obtiendrez pas les résultats appropriés lors d'une vérification ==. Au lieu de dépendre de l'égalité d'instance, nous avons utilisé un compteur simple et un appel de vidage lorsqu'il atteint zéro, ce qu'il sera lorsque le XCTestSuite de niveau supérieur termine son exécution. Il existe probablement des moyens plus intelligents de procéder.

Tout d'abord, nous avons dû définir "Générer des fichiers de couverture de test = OUI" et "Flux de programme d'instrument = OUI" dans les deux les cibles de test et d'application principale.

#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

Une étape supplémentaire était nécessaire, car l'appel + initialize n'était pas effectué sur l'observateur lorsqu'il était inclus dans la cible de test.

Dans l'AppDelegate, ajoutez ce qui suit:

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

Voici une autre solution qui évite d'avoir à modifier votre AppDelegate

IApplication + Instrumented.m (mettez ceci dans votre cible principale):

@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 (mettez ceci dans votre cible de test):

@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
1
Jasper Blues

GCOV Flush in - (void) applicationWillTerminate n'a pas fonctionné pour moi, je pense parce que mon application fonctionne en arrière-plan.

J'ai également défini "Générer des fichiers de couverture de test = OUI" et "Flux de programme d'instrument = OUI" mais pas de fichiers gcda.

Ensuite, j'ai exécuté "__gcov_flush ()" dans - (void) tearDown de la TestClass, ce qui m'a donné des fichiers gcda pour ma TestClass;)

J'ai ensuite créé la fonction suivante dans mon 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

J'ai appelé [AppDelegate gcovFlush] dans mon - (void) tearDown et voilá, il y a mes fichiers gcda;)

J'espère que ça aide, bye Chris

0
sPooKee

Le processus est un peu différent si vous utilisez Specta, car il fait son propre swizzling. Ce qui suit fonctionne pour moi:

Pack de test:

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

Vous devez ensuite activer votre sous-classe de reporter personnalisée en définissant la variable d'environnement SPECTA_REPORTER_CLASS à MyReporter dans la section Exécuter de votre schéma principal.

0
Jonathan Crooke

- (void)applicationWillTerminate:(UIApplication*)application doit être défini dans votre délégué d'application, pas dans la classe observateur.

Je n'ai eu aucun problème de bibliothèque. "-lgov" n'est pas nécessaire et vous n'avez pas besoin d'ajouter de bibliothèques. La couverture est prise en charge directement par le compilateur LLVM.

0
Sulthan