Why can't code inside unit tests find bundle resources?

CocoaUnit TestingXcodeNsbundleOctest

Cocoa Problem Overview


Some code I am unit testing needs to load a resource file. It contains the following line:

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

In the app it runs just fine, but when run by the unit testing framework pathForResource: returns nil, meaning it could not locate foo.txt.

I've made sure that foo.txt is included in the Copy Bundle Resources build phase of the unit test target, so why can't it find the file?

Cocoa Solutions


Solution 1 - Cocoa

When the unit test harness runs your code, your unit test bundle is NOT the main bundle.

Even though you are running tests, not your application, your application bundle is still the main bundle. (Presumably, this prevents the code you are testing from searching the wrong bundle.) Thus, if you add a resource file to the unit test bundle, you won't find it if search the main bundle. If you replace the above line with:

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

Then your code will search the bundle that your unit test class is in, and everything will be fine.

Solution 2 - Cocoa

A Swift implementation:

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 provides ways to discover the main and test paths for your 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

In Xcode 6|7|8|9, a unit-test bundle path will be in Developer/Xcode/DerivedData something like ...

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

... which is separate from the Developer/CoreSimulator/Devices regular (non-unit-test) bundle path:

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

Also note the unit test executable is, by default, linked with the application code. However, the unit test code should only have Target Membership in just the test bundle. The application code should only have Target Membership in the application bundle. At runtime, the unit test target bundle is injected into the application bundle for execution.

Swift Package Manager (SPM) 4:

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

Note: By default, the command line swift test will create a MyProjectPackageTests.xctest test bundle. And, the swift package generate-xcodeproj will create a MyProjectTests.xctest test bundle. These different test bundles have different paths. Also, the different test bundles may have some internal directory structure and content differences.

In either case, the .bundlePath and .bundleURL will return the path of test bundle currently being run on macOS. However, Bundle is not currently implemented for Ubuntu Linux.

Also, command line swift build and swift test do not currently provide a mechanism for copying resources.

However, with some effort, it is possible to set up processes for using the Swift Package Manger with resources in the macOS Xcode, macOS command line, and Ubuntu command line environments. One example can be found here: 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref

See also: Use resources in unit tests with Swift Package Manager

Swift Package Manager (SwiftPM) 5.3

Swift 5.3 includes Package Manager Resources SE-0271 evolution proposal with "Status: Implemented (Swift 5.3)". :-)

> Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests. > > * Add a new resources parameter in target and testTarget APIs to allow declaring resource files explicitly. > > SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.

> lang-swift > // 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) >

Example

// 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"),
      ]),

Current Issue

Xcode

Bundle.module is generated by SwiftPM (see Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle when built by Xcode.

A comparable approach in Xcode would be to manually add a Resources reference folder to the module, add an Xcode build phase copy to put the Resource into some *.bundle directory, and add a #ifdef Xcode compiler directive for the Xcode build to work with the resources.

#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

Solution 3 - Cocoa

With swift Swift 3 the syntax self.dynamicType has been deprecated, use this instead

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

or

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

Solution 4 - Cocoa

Confirm that the resource is added to the test target.

enter image description here

Solution 5 - Cocoa

if you have multiple target in your project then you need to add resources between different target available in the Target Membership and you may need to switch between different Target as 3 steps shown in the figure below

enter image description here

Solution 6 - Cocoa

I had to ensure this General Testing checkbox was set this General Testing checkbox was set

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbenzadoView Question on Stackoverflow
Solution 1 - CocoabenzadoView Answer on Stackoverflow
Solution 2 - Cocoal --marc lView Answer on Stackoverflow
Solution 3 - CocoaMarkHimView Answer on Stackoverflow
Solution 4 - CocoamishimayView Answer on Stackoverflow
Solution 5 - CocoaSultan AliView Answer on Stackoverflow
Solution 6 - Cocoadrew..View Answer on Stackoverflow