208 votes

Pourquoi le code contenu dans les tests unitaires ne peut-il pas trouver les ressources du paquet ?

Un code que je teste unitairement doit charger un fichier de ressources. Il contient la ligne suivante :

NSString *path = [[NSBundle mainBundle] pathForResource:@"foo" ofType:@"txt"];

Dans l'application, il fonctionne parfaitement, mais lorsqu'il est exécuté par le framework de test unitaire pathForResource: renvoie un résultat nul, ce qui signifie qu'il n'a pas pu localiser foo.txt .

Je me suis assuré que foo.txt est inclus dans le Ressources pour le faisceau de copies phase de construction de la cible du test unitaire, alors pourquoi ne trouve-t-il pas le fichier ?

336voto

benzado Points 36367

Lorsque le harnais de test unitaire exécute votre code, votre paquet de test unitaire est PAS le faisceau principal.

Même si vous exécutez des tests, et non votre application, votre bundle d'application est toujours le bundle principal. (On peut supposer que cela empêche le code que vous testez de chercher dans le mauvais bundle). Ainsi, si vous ajoutez un fichier de ressources au bundle des tests unitaires, vous ne le trouverez pas si vous cherchez dans le bundle principal. Si vous remplacez la ligne ci-dessus par :

NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:@"foo" ofType:@"txt"];

Votre code recherchera alors le bundle dans lequel se trouve votre classe de test unitaire, et tout ira bien.

0 votes

Ne fonctionne pas pour moi. Toujours le bundle de construction et non le bundle de test.

1 votes

@Chris Dans la ligne d'exemple, je suppose que self fait référence à une classe du paquet principal, et non à la classe du scénario de test. Remplacer [self class] avec n'importe quelle classe de votre faisceau principal. Je vais modifier mon exemple.

0 votes

@benzado Le bundle est toujours le même (build), ce qui est correct je pense. Parce que lorsque j'utilise self ou l'AppDelegate, les deux sont situés dans le bundle principal. Lorsque je vérifie les phases de construction de la cible principale, les deux fichiers sont présents. Mais ce que je veux, c'est différencier le bundle principal du bundle de test au moment de l'exécution. Le code où j'ai besoin du bundle est dans le bundle principal. J'ai le problème suivant. Je charge un fichier png. Normalement ce fichier n'est pas dans le bundle principal car l'utilisateur le télécharge depuis un serveur. Mais pour un test, je veux utiliser un fichier du bundle de test sans le copier dans le bundle principal.

113voto

l --marc l Points 3606

Une implémentation Swift :

Swift 2

let testBundle = NSBundle(forClass: self.dynamicType)
let fileURL = testBundle.URLForResource("imageName", withExtension: "png")
XCTAssertNotNil(fileURL)

Swift 3, Swift 4

let testBundle = Bundle(for: type(of: self))
let filePath = testBundle.path(forResource: "imageName", ofType: "png")
XCTAssertNotNil(filePath)

Bundle fournit des moyens de découvrir les chemins principaux et de test pour votre configuration :

@testable import Example

class ExampleTests: XCTestCase {

    func testExample() {
        let bundleMain = Bundle.main
        let bundleDoingTest = Bundle(for: type(of: self ))
        let bundleBeingTested = Bundle(identifier: "com.example.Example")!

        print("bundleMain.bundlePath : \(bundleMain.bundlePath)")
        // …/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Agents
        print("bundleDoingTest.bundlePath : \(bundleDoingTest.bundlePath)")
        // …/PATH/TO/Debug/ExampleTests.xctest
        print("bundleBeingTested.bundlePath : \(bundleBeingTested.bundlePath)")
        // …/PATH/TO/Debug/Example.app

        print("bundleMain = " + bundleMain.description) // Xcode Test Agent
        print("bundleDoingTest = " + bundleDoingTest.description) // Test Case Bundle
        print("bundleUnderTest = " + bundleBeingTested.description) // App Bundle

Dans Xcode 6|7|8|9, une test unitaire chemin du paquet sera dans Developer/Xcode/DerivedData quelque chose comme ...

/Users/
  UserName/
    Library/
      Developer/
        Xcode/
          DerivedData/
            App-qwertyuiop.../
              Build/
                Products/
                  Debug-iphonesimulator/
                    AppTests.xctest/
                      foo.txt

... qui est séparée de la Developer/CoreSimulator/Devices chemin normal (non testé par l'unité) du faisceau de données :

/Users/
  UserName/
    Library/
    Developer/
      CoreSimulator/
        Devices/
          _UUID_/
            data/
              Containers/
                Bundle/
                  Application/
                    _UUID_/
                      App.app/

Notez également que l'exécutable du test unitaire est, par défaut, lié au code de l'application. Cependant, le code de test unitaire ne devrait avoir que l'adhésion à Target dans le seul bundle de test. Le code de l'application ne doit avoir une appartenance à la cible que dans le bundle de l'application. Au moment de l'exécution, le bundle cible du test unitaire est injecté dans le paquet d'applications pour être exécuté .

Gestionnaire de paquets Swift (SPM) 4 :

let testBundle = Bundle(for: type(of: self)) 
print("testBundle.bundlePath = \(testBundle.bundlePath) ")

Remarque : Par défaut, la ligne de commande swift test créera un MyProjectPackageTests.xctest test bundle. Et, le swift package generate-xcodeproj créera un MyProjectTests.xctest test bundle. Ces différents lots de tests ont différents chemins . De plus, les différents paquets de test peuvent avoir certains la structure des répertoires internes et les différences de contenu .

Dans les deux cas, le .bundlePath y .bundleURL renverra le chemin du paquet de tests en cours d'exécution sur macOS. Cependant, Bundle n'est pas actuellement implémenté pour Ubuntu Linux.

De même, la ligne de commande swift build y swift test ne fournissent pas actuellement un mécanisme de copie des ressources.

Cependant, avec un peu d'effort, il est possible de mettre en place des processus pour utiliser le Swift Package Manger avec des ressources dans les environnements macOS Xcode, macOS command line et Ubuntu command line. Un exemple peut être trouvé ici : 004.4'2 SW Dev Swift Package Manager (SPM) avec ressources Qref

Voir aussi : Utiliser des ressources dans les tests unitaires avec le gestionnaire de paquets Swift

Gestionnaire de paquets Swift (SwiftPM) 5.3

Swift 5.3 comprend Ressources du gestionnaire de paquets SE-0271 proposition d'évolution avec "Statut : Implémenté (Swift 5.3) ". :-)

Les ressources ne sont pas toujours destinées à être utilisées par les clients du paquet. Une utilisation des ressources peut inclure des tests de montage qui ne sont nécessaires que pour les tests unitaires. De telles ressources ne seraient pas incorporées dans les clients du paquet avec le code de la bibliothèque, mais seraient uniquement utilisées lors de l'exécution des tests du paquet.

  • Ajouter un nouveau resources dans target y testTarget pour permettre de déclarer explicitement les fichiers de ressources.

SwiftPM utilise les conventions du système de fichiers pour déterminer l'ensemble des fichiers sources qui appartiennent à chaque cible d'un paquetage : plus précisément, les fichiers sources d'une cible sont ceux qui sont situés sous le "répertoire cible" désigné pour la cible. Par défaut, il s'agit d'un répertoire portant le même nom que la cible et situé dans "Sources" (pour une cible normale) ou "Tests" (pour une cible de test), mais cet emplacement peut être personnalisé dans le manifeste du paquet.

// Get path to DefaultSettings.plist file.
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")

// Load an image that can be in an asset archive in a bundle.
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))

// Find a vertex function in a compiled Metal shader library.
let shader = try mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")

// Load a texture.
let texture = MTKTextureLoader(device: mtlDevice).newTexture(name: "Grass", scaleFactor: 1.0, bundle: Bundle.module, options: options)

Ejemplo

// swift-tools-version:5.3
import PackageDescription

  targets: [
    .target(
      name: "CLIQuickstartLib",
      dependencies: [],
      resources: [
        // Apply platform-specific rules.
        // For example, images might be optimized per specific platform rule.
        // If path is a directory, the rule is applied recursively.
        // By default, a file will be copied if no rule applies.
        .process("Resources"),
      ]),
    .testTarget(
      name: "CLIQuickstartLibTests",
      dependencies: [],
      resources: [
        // Copy directories as-is. 
        // Use to retain directory structure.
        // Will be at top level in bundle.
        .copy("Resources"),
      ]),

Numéro actuel

Xcode

Bundle.module est généré par SwiftPM (voir Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor() ) et ne sont donc pas présents dans Fondation.Bundle quand il est construit par Xcode.

Une approche comparable dans Xcode serait d'ajouter manuellement un fichier Resources au module, ajoutez une phase de construction Xcode copy pour mettre le Resource dans certains *.bundle et ajoutez un #ifdef Xcode pour que la compilation de Xcode fonctionne avec les ressources.

#if Xcode 
extension Foundation.Bundle {

  /// Returns resource bundle as a `Bundle`.
  /// Requires Xcode copy phase to locate files into `*.bundle`
  /// or `ExecutableNameTests.bundle` for test resources
  static var module: Bundle = {
    var thisModuleName = "CLIQuickstartLib"
    var url = Bundle.main.bundleURL

    for bundle in Bundle.allBundles 
      where bundle.bundlePath.hasSuffix(".xctest") {
      url = bundle.bundleURL.deletingLastPathComponent()
      thisModuleName = thisModuleName.appending("Tests")
    }

    url = url.appendingPathComponent("\(thisModuleName).bundle")

    guard let bundle = Bundle(url: url) else {
      fatalError("Bundle.module could not load: \(url.path)")
    }

    return bundle
  }()

  /// Directory containing resource bundle
  static var moduleDir: URL = {
    var url = Bundle.main.bundleURL
    for bundle in Bundle.allBundles 
      where bundle.bundlePath.hasSuffix(".xctest") {
      // remove 'ExecutableNameTests.xctest' path component
      url = bundle.bundleURL.deletingLastPathComponent()
    }
    return url
  }()

}
#endif

17voto

mhimmelz Points 11

Avec swift Swift 3, la syntaxe self.dynamicType a été déprécié, utilisez ceci à la place

let testBundle = Bundle(for: type(of: self))
let fooTxtPath = testBundle.path(forResource: "foo", ofType: "txt")

o

let fooTxtURL = testBundle.url(forResource: "foo", withExtension: "txt")

5voto

md_develop Points 548

Confirmez que la ressource est ajoutée à la cible de test.

enter image description here

2voto

Sultan Ali Points 412

Si vous avez plusieurs cibles dans votre projet, vous devez ajouter des ressources entre les différentes cibles disponibles dans la base de données. Membres ciblés et vous pouvez avoir besoin de basculer entre différents Cible en 3 étapes, comme le montre la figure ci-dessous

enter image description here

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X