NSURL to file path in test bundle with XCTest
Asked Answered
R

8

85

I am trying to write an iOS app using TDD and the new XCTest framework. One of my methods retrieves a file from the internet (given a NSURL object) and stores it in the user's documents. The signature of the method is similar to:

- (void) fetchAndStoreImage:(NSURL *)imageUrl

I'm trying to write the test for this method in a way that it doesn't fail if there is no connection to the internet. My approach (taken from a previous question) is to call the method using a NSURL to an image in the local file system.

When a new project with unit tests enabled is created, the Tests directory has a subdirectory named 'Supporting Files'. I suppose that is where my test images should go. My question is how can I get a NSURL object that points to an image in this directory, as I wouldn't want test images to be bundled with the application. Any help is appreciated.

Ruffina answered 11/10, 2013 at 2:3 Comment(1)
This answer fixes that problem without changing code https://mcmap.net/q/128929/-nsbundle-pathforresource-returns-nilMoria
Y
135

In fact, the [NSBundle mainBundle] when running a UnitTest is not the path of your app, but is /Developer/usr/bin, so this will not work.

The way to get resources in a unit test is here: OCUnit & NSBundle

In short, use:

[[NSBundle bundleForClass:[self class]] resourcePath]

or in your case:

[[NSBundle bundleForClass:[self class]] resourceURL]
Youngling answered 28/10, 2013 at 13:44 Comment(2)
@pshah Yes I believe so, though i haven't tried it myself.Youngling
This is extremely helpful. By the way, in Swift it's NSBundle(forClass: self.dynamicType)Vitebsk
H
64

Swift 5.3

Note: Swift 5.3 includes Package Manager Resources SE-0271 capabilities which can be used with application bundle and test bundle resources.

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.

Swift 4, 5:

let testBundle = Bundle(for: type(of: self))
guard var fileUrl = testBundle.url(forResource: "imageName", withExtension: "png") 
  else { fatalError() }

// path approach
guard let filePath = testBundle.path(forResource: "dataName", ofType: "csv")
  else { fatalError() }
let fileUrl = URL(fileURLWithPath: 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

The Xcode URL will be in Developer/Xcode/DerivedData something like ...

file:///Users/
  UserName/
    Library/
      Developer/
        Xcode/
          DerivedData/
            App-qwertyuiop.../
              Build/
                Products/
                  Debug-iphonesimulator/
                    AppTests.xctest/
                      imageName.png

... which is separate from Developer/CoreSimulator/Devices URL

file:///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 for Swift 4.

Also, the Swift 4 command line swift build and swift test do not 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

Heddle answered 5/6, 2015 at 19:4 Comment(6)
Thanks! Trying your code, it should be URLForResource("imageName", withExtension: "png")Creamer
@Creamer The example is now corrected to withExtension. Thx. withType would be for retrieving a path pathForResource("imageName", ofType: "png") instead of a URL.Heddle
what is self.dynamicType?Andria
@Andria self references the enclosing test class SomeCaseTests: XCTestCase {..}. And, .dynamicType evaluates to the value of the runtime type of the class. see Apple Dev document and scroll to Dynamic Type Expression section.Heddle
@l--marcl I'm using Xcode 7 but I still have to use self.dynamicType otherwise I get an error trying to grab the bundle. Are you sure you can just feed NSBundle(forClass:) self?Benzoin
@Benzoin With an update to Xcode Version 7.0.1 (7A1001), self.dynamicType started working for me again. I also added some more approaches to getting the XCText path to make the answer more robust. Thanks for the comment.Heddle
B
15

Just to append to correct answer, this is example how to get filePath for a file in your UnitTests/Supporting Files:

NSString *filePath = [[[NSBundle bundleForClass:[self class]] resourcePath] stringByAppendingPathComponent:@"YourFileName.json"];
XCTAssertNotNil(filePath);

This probably will be useful for someone.

Borghese answered 13/11, 2014 at 18:49 Comment(0)
H
3
  1. You can reference bundle files like in any target.
  2. Check if the file is Copyed in the Copy Bundle Resources build phase (of your test target)
  3. To access to the local file :

    NSURL*imageUrl=[[NSBundle mainBundle]URLForResource:@"imageName" withExtension:@"png"];
    

You can make asynchronous access and wait for the response using : https://github.com/travisjeffery/TRVSMonitor

If you have added : dataset1.json in the test target (2) :

NSString *p=[[NSBundle mainBundle] pathForResource:@"dataset1" ofType:@"json"];
NSLog(@"%@",p);

2013-10-29 15:49:30.547 PlayerSample[13771:70b] WT(0): /Users/bpds/Library/Application Support/iPhone Simulator/7.0/Applications/7F78780B-684A-40E0-AA35-A3B5D8AA9DBD/PlayerSample.app.app/dataset1.json

Hyposensitize answered 28/10, 2013 at 10:4 Comment(2)
I tried and it doesn't seem to work. Apparently images in the test bundle don't get copied to the main bundle. The way to go is in Ben's answer, you have to get the test bundle by using [NSBundle bundleForClass:[self class]] from the test case class.Ruffina
Interestingly this does appear to work (at least for ios simulator targets.) You absolutely need to make sure you add the resource to the test target's copy bundle resources build phase.Jollenta
K
2

Swift version based on accepted answer:

let url = URL(fileURLWithPath: Bundle(for: type(of: self)).path(forResource: "my", ofType: "json") ?? "TODO: Proper checking for file")
Khanna answered 20/6, 2019 at 15:38 Comment(0)
C
1

Here's the Swift version of this, Xcode 7, iOS 9, etc.

let testBundle = NSBundle(forClass: self.dynamicType)
let path = testBundle.pathForResource("someImage", ofType: "jpg")
XCTAssertNotNil(path)

Note: someImage.jpg must be included in your test target.


Edit: Swift 5

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "someImage", ofType: "jpg")
XCTAssertNotNil(path)
Cheviot answered 30/12, 2015 at 0:6 Comment(0)
P
1

The problem I was facing was that application code was trying to access the main bundle for things like bundleIdentifier and since the main bundle wasn't my unit test project it would return nil.

A hack around this that works in both Swift 3.0.1 and Objective-C is to create an Objective-C category on NSBundle and include it in your unit test project. You don't need a bridging header or anything. This category will get loaded and now when you application code asks for the main bundle your category will return the unit test bundle.

@interface NSBundle (MainBundle)

+(NSBundle *)mainBundle;

@end

@implementation NSBundle (Main)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
+(NSBundle *)mainBundle
{
    return [NSBundle bundleForClass:[SomeUnitTest class]];
}
#pragma clang diagnostic pop

@end
Palp answered 18/12, 2016 at 22:46 Comment(0)
P
0

Swift 5

let testBundle = Bundle(for: self)

Using let testBundle = Bundle(for: type(of: self)), found in some of the answers above, would not compile and instead consistently produced an error of Segmentation fault: 11 for me.

Pacer answered 14/8, 2020 at 22:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.