XCTest: Load file from disk without Bundle on all platforms (Xcode, SPM Mac/Linux)

Viewed 1425

I am looking for a way to load files from disk within an XCTestCase that does not depend on Bundle.

Bundle works well when running the tests from Xcode (or with xcodebuild on the terminal), but bundles are part of the Xcode project and not available to Swift Package Manager (when running swift test), neither on the Mac nor in Linux.

Is there a way to specify the current directory where tests should be run that works in all platforms? Or maybe there is a way to determine where the tests are located that also works on all platforms?

FileManager.default.currentDirectoryPath only returns the current execution path (working directory).

2 Answers

This method seems to work for me, and should work with both Xcode 11 and from the command line. Copy of the answer I just wrote here.

struct Resource {
  let name: String
  let type: String
  let url: URL

  init(name: String, type: String, sourceFile: StaticString = #file) throws {
    self.name = name
    self.type = type

    // The following assumes that your test source files are all in the same directory, and the resources are one directory down and over
    // <Some folder>
    //  - Resources
    //      - <resource files>
    //  - <Some test source folder>
    //      - <test case files>
    let testCaseURL = URL(fileURLWithPath: "\(sourceFile)", isDirectory: false)
    let testsFolderURL = testCaseURL.deletingLastPathComponent()
    let resourcesFolderURL = testsFolderURL.deletingLastPathComponent().appendingPathComponent("Resources", isDirectory: true)
    self.url = resourcesFolderURL.appendingPathComponent("\(name).\(type)", isDirectory: false)
  }
}

Usage:

final class SPMTestDataTests: XCTestCase {
  func testExample() throws {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct
    // results.
    XCTAssertEqual(SPMTestData().text, "Hello, World!")

    let file = try Resource(name: "image", type: "png")
    let image = UIImage(contentsOfFile: file.url.path)
    print(image)
  }
}

enter image description here

I found the key of using #file here

Seems like when running tests with Swift Package Manager (swift test), the working directory is the root of the project. This allows for easily loading resources from disk using a relative path (eg. ./Tests/Resources).

After evaluating different options, I wrote the following class to retrieve the path both from the test bundle, if available, or from a given path.

class Resource {
    static var resourcePath = "./Tests/Resources"

    let name: String
    let type: String

    init(name: String, type: String) {
        self.name = name
        self.type = type
    }

    var path: String {
        guard let path: String = Bundle(for: Swift.type(of: self)).path(forResource: name, ofType: type) else {
            let filename: String = type.isEmpty ? name : "\(name).\(type)"
            return "\(Resource.resourcePath)/\(filename)"
        }
        return path
    }
}

Resources must be added to all test targets for them to be available in the bundle. The above class could be updated to support multiple resource paths, by file extension, for instance.

Resources can be then loaded from an XCTest as needed:

let file = Resource(name: "datafile", type: "csv")
let content = try String(contentsOfFile: file)
let image = try UIImage(contentsOfFile: Resource(name: "image", type: "png"))

Extensions can be added to Resource to load the contents in different formats.

extension Resource {
    var content: String? {
        return try? String(contentsOfFile: path).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
    }

    var base64EncodedData: Data? {
        guard let string = content, let data = Data(base64Encoded: string) else {
            return nil
        }
        return data
    }

    var image: Image? {
        return try? UIImage(contentsOfFile: path)
    }
}

And be used as:

let json = Resource(name: "datafile", type: "json").contents
let image = Resource(name: "image", type: "jpeg").image
Related