Why can't code inside unit tests find bundle resources?
Solution 1
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
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 intarget
andtestTarget
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.
// 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
- Swift 5.3 SPM Resources in tests uses wrong bundle path?
- Swift Package Manager - Resources in test targets
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
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
Confirm that the resource is added to the test target.
Solution 5
Related videos on Youtube
benzado
I am a software engineer/developer/programmer in New York City. As of August 2015, my main thing is LogCheck. My side thing is Heroic Software. (So side, it's not worth linking to.) One of these days, I'll start blogging again.
Updated on July 31, 2020Comments
-
benzado almost 4 years
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 locatefoo.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? -
Chris almost 12 yearsDoes not work for me. Still the build bundle and not the test bundle.
-
benzado almost 12 years@Chris In the sample line I'm assuming
self
refers to a class in the main bundle, not the test case class. Replace[self class]
with any class in your main bundle. I'll edit my example. -
Chris almost 12 years@benzado The bundle is still the same (build), which is correct I think. Because when I am using self or the AppDelegate, both are located in the main bundle. When I check the Build Phases of the main target both files are in. But what I want to differ between main and test bundle at run time. The code where I need the bundle is in the main bundle. I have the following a problem. I am loading a png file. Normally this file is not in the main bundle due the user downloads it from a server. But for a test I want to use a file from the test bundle without copying it into the main bundle.
-
benzado almost 12 years@Chris I made a mistake with my previous edit, and edited the answer again. At test time, the app bundle is still the main bundle. If you want to load a resource file that is in the unit test bundle, you need to use
bundleForClass:
with a class in the unit test bundle. You should get the path of the file in your unit test code, then pass the path string along to your other code. -
Chris almost 12 yearsThis works but how can I distinguish between a run-deploy and a test-deploy? Based on the fact if it is a test I need a resource from the test bundle in a class in the main bundle. If it is a regular 'run' I need a resource from the main bundle and not the test bundle. Any idea?
-
benzado almost 12 years@Chris No idea. You should open a new Stack Overflow question.
-
ArtOfWarfare about 10 years@Chris - You should write your main classes so that they are testable, not so that they behave differently based on whether they're being tested or not. You'll end up writing new code which only runs in your tests and verifying that works without actually verifying that the code you run outside of your tests works.
-
Chris about 10 years@ArtOfWarfare Firstly, of course, you're right! ;) Secondly, this post is 2 years old. At that time I was learning objective-C. :)
-
Carlos Ricardo about 10 yearsThis answer also fixes that problem without changing code stackoverflow.com/a/24330617/468868
-
dgatwood over 7 yearsAdding resources to the test bundle makes the test results largely invalid. After all, a resource could easily be in the test target but not in the app target, and your tests would all pass, but the app would burst into flames.
-
Rocket Garden over 6 yearsFor Swift 4 as well, you can use Bundle(for: type(of: self))
-
Christopher Pickslay over 3 yearsWhat @dgatwood said. It also doesn't fix the problem