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();
}
}
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 .
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.
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:
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
Pour enregistrer la classe d'observateur de test, ajoutez le code suivant à la section @implementation
de l'un des deux:
ExampleAppDelegate.m
, à l'intérieur du groupe ExampleApp 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.
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
}
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.xcconfig
associé à la configuration "Debug" du projet , avec les déclarations suivantes:
GCC_GENERATE_TEST_COVERAGE_FILES = YES
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES
Assurez-vous que tous les fichiers Ne faites pas cela: le code d'application appartient à la cible de l'application, le code de test appartient à la cible de test !.m
du projet sont également inclus dans la phase de compilation "Compiler les sources" de la cible "Exemples de tests".
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
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;
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;
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.
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
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];
}
É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
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
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
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.
- (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.