web-dev-qa-db-fra.com

Comment exécuter un code d'installation unique avant d'exécuter un XCTest

J'ai le problème suivant. Je veux exécuter un morceau de code avant que toutes les classes de test soient exécutées. Par exemple: je ne veux pas que mon jeu utilise le singleton SoundEngine pendant l'exécution, mais le SilentSoundEngine. Je voudrais activer le SilentSoundEngine une fois pas dans tous les tests. Tous mes tests ressemblent à ceci:

class TestBasketExcercise : XCTestCase {        
    override func setUp() {
        SilentSoundEngine.activate () // SoundEngine is a singleton
    }
    // The tests 
}

-Edit- La plupart des réponses visent à fournir une superclasse personnalisée pour TestCase. Je recherche un moyen plus général et plus propre de fournir l'environnement que tous les tests doivent exécuter. N'y a-t-il pas quelque part une fonction "principale"/une fonction similaire à Appdelegate pour les tests?

34
Raymond

TL; DR:
Comme indiqué ici , vous devez déclarer un NSPrincipalClass dans votre Info.plist de test-cibles. Exécutez tout le code de configuration unique à l'intérieur de l'init de cette classe, car "XCTest crée automatiquement une seule instance de cette classe lorsque le bundle de test est chargé", ainsi tout votre code de configuration unique sera exécuté une fois lors du chargement l'ensemble de tests.


Un peu plus verbeux:

Pour répondre d'abord à l'idée de votre modification:

Afaik, il n'y a pas de main() pour le bundle de test, puisque les tests sont injectés dans votre cible principale en cours d'exécution, vous devrez donc ajouter le code de configuration unique dans la main() de votre cible principale avec une vérification au moment de la compilation (ou au moins une exécution) si la cible est utilisée pour exécuter des tests. Sans cette vérification, vous risquez d'activer le SilentSoundEngine lorsque vous exécutez la cible normalement, ce qui, je suppose, n'est pas souhaitable, car le nom de la classe implique que ce moteur sonore ne produira aucun son et honnêtement, qui veut cela? :)

Il existe cependant une fonctionnalité semblable à AppDelegate, j'y reviendrai à la fin de ma réponse (si vous êtes impatient, c'est sous l'en-tête "Une autre approche (plus spécifique à XCTest)").


Maintenant, divisons cette question en deux problèmes principaux:

  1. Comment pouvez-vous vous assurer que le code que vous souhaitez exécuter exactement une fois lors de l'exécution des tests est réellement exécuté exactement une fois lors de l'exécution des tests
  2. devez-vous exécuter ce code, pour qu'il ne ressemble pas à un laid hack et qu'il fonctionne donc sans que vous ayez y penser et se souvenir des étapes nécessaires à chaque fois que vous écrivez une nouvelle suite de tests

Concernant le point 1:

Comme @Martin R l'a mentionné correctement dans ses commentaires à cette réponse à votre question, remplacer +load N'est plus possible à partir de Swift 1.2 (qui est histoire ancienne maintenant: D), et dispatch_once() n'est plus disponible dans Swift 3.


Une approche

De toute façon, lorsque vous essayez d'utiliser dispatch_once, Xcode (> = 8) est toujours très intelligent et suggère d'utiliser à la place des globaux initialisés paresseusement. Bien sûr, le terme global tend à faire en sorte que tout le monde se livre à la peur et à la panique, mais vous pouvez bien sûr limiter leur portée en les rendant privés/fileprivate (ce qui fait la même chose pour les déclarations de niveau fichier), donc vous ne 'pollue pas votre espace de noms.

À mon humble avis, ils sont en fait un joli motif sympa (encore, la dose fait le poison ...) qui peut ressembler à ceci, par exemple:

private let _doSomethingOneTimeThatDoesNotReturnAResult: Void = {
    print("This will be done one time. It doesn't return a result.")
}()

private let _doSomethingOneTimeThatDoesReturnAResult: String = {
    print("This will be done one time. It returns a result.")
    return "result"
}()

for i in 0...5 {
    print(i)
    _doSomethingOneTimeThatDoesNotReturnAResult
    print(_doSomethingOneTimeThatDoesReturnAResult)
}

Cela imprime:

Cela sera fait une seule fois. Il ne renvoie aucun résultat.
Cela sera fait une fois. Il renvoie un résultat.
0
résultat
1
résultat
2
résultat
3
résultat
4
résultat
5
résultat

Remarque: assez intéressant, les let privés sont évalués avant même que la boucle ne démarre, ce que vous pouvez voir car si ce n'était pas le cas, le 0 aurait été la toute première impression. Lorsque vous commentez la boucle, elle imprimera toujours les deux premières lignes (c'est-à-dire évaluer les let).
Cependant, je suppose que c'est un comportement spécifique au terrain de jeu car comme indiqué ici et ici , les globaux sont normalement initialisés la première fois qu'ils sont référencés quelque part, donc ils ne doit pas être évalué lorsque vous commentez la boucle.


Une autre approche (plus spécifique à XCTest)

(Cela résout en fait les points 1 et 2 ...)

Comme la société de Cupertino déclare ici , il existe un moyen d'exécuter le code de configuration de pré-test unique.

Pour y parvenir, vous créez une classe de configuration factice (peut-être l'appeler TestSetup?) Et mettez tout le code de configuration unique dans son init:

class TestSetup: NSObject {
    override init() {
        SilentSoundEngine.activate()
    }
}

Notez que la classe doit hériter de NSObject, car Xcode essaie d'instancier "l'instance unique de cette classe" en utilisant +new, donc si la classe est une pure classe Swift, cela se produira:

*** NSForwarding: warning: object 0x11c2d01e0 of class 'YourTestTargetsName.TestSetup' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector +[YourTestTargetsName.TestSetup new]

Ensuite, vous déclarez cette classe comme PrincipalClass dans votre fichier Info.plist de test-bundles: Declaring the PrincipalClass in Info.plist

Notez que vous devez utiliser le nom de classe complet (c'est-à-dire YourTestTargetsName.TestSetup par rapport à seulement TestSetup), donc la classe est trouvée par Xcode ( Merci, zneak ...).

Comme indiqué dans la documentation de XCTestObservationCenter, "XCTest crée automatiquement une seule instance de cette classe lorsque le bundle de test est chargé", donc tout votre code de configuration unique sera exécuté dans l'init de TestSetup lors du chargement du bundle de test.

44
Jan Nash

De Écriture des classes et méthodes de test :

Vous pouvez éventuellement ajouter des méthodes personnalisées pour la configuration de la classe (+ (void)setUp) et démontage (+ (void)tearDown) également, qui s'exécutent avant et après toutes les méthodes de test de la classe.

Dans Swift ce serait des méthodes class:

override class func setUp() {
    super.setUp()
    // Called once before all tests are run
}

override class func tearDown() {
    // Called once after all tests are run
    super.tearDown()
}
56
Martin R

Si vous créez une superclasse sur laquelle votre scénario de test sera basé, vous pouvez exécuter une configuration universelle dans la superclasse et effectuer la configuration spécifique dont vous pourriez avoir besoin dans les sous-classes. Je suis plus familier avec Obj-C que Swift et je n'ai pas encore eu l'occasion de tester cela, mais cela devrait être proche.

// superclass
class SuperClass : XCTestCase {        
    override func setUp() {
        SilentSoundEngine.activate () // SoundEngine is a singleton
    }
}

// subclass
class Subclass : Superclass {
    override func setUp() {
        super.setup()
    }
}
3
tsmith1024