Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Last active July 9, 2023 11:58
Show Gist options
  • Save brennanMKE/b5453070f47ad2418f691884b1e9dfc0 to your computer and use it in GitHub Desktop.
Save brennanMKE/b5453070f47ad2418f691884b1e9dfc0 to your computer and use it in GitHub Desktop.
Swift Package Test Resources

Swift Package Test Resources

Loading resources for use with unit tests in a Swift package does not appear to be a simple task. Apple's article Bundling Resources with a Swift Package recommends using Bundle.module but Bundle does not include a definition of this property. It is actually generated when using SPM with a target which defines resources. Using it means other build systems such as Xcode projects and CocoaPods cannot be used once Bundle.module is used.

An alternative to getting the path is to use #file to implement a findUp function which will look for the root directory. From there it will append Resources which is where all resources will be located. It is not necessary to declare them as a part of the package since this is just for unit tests when the source will be available when tests are run.

The tests find, load and decode the sample resource with supporting functions and a lazy property. This way JSON resources which can be loaded with Codable can be used by tests without updating Package.swift with a list of resources.


import XCTest
struct Sample: Codable {
let name: String
}
final class ResourceTests: XCTestCase {
let rootFilename = "Package.swift"
let resourcesDir = "Resources"
func testFindingSampleJson() throws {
let filename = "Sample.json"
guard let sampleJsonURL = findResource(filename: filename) else {
XCTFail("Failed to find JSON URL for \(filename)")
return
}
let exists = FileManager.default.fileExists(atPath: sampleJsonURL.path)
print("URL: \(sampleJsonURL.path)")
XCTAssertTrue(exists)
}
func testLoadingSampleJson() throws {
let filename = "Sample.json"
guard let data = loadResource(filename: filename) else {
XCTFail("Failed to load JSON URL for \(filename)")
return
}
XCTAssertFalse(data.isEmpty)
}
func testDecodingSampleJson() throws {
let filename = "Sample.json"
var sample: Sample? = nil
XCTAssertNoThrow(sample = try decodeResource(filename: filename, type: Sample.self))
XCTAssertNotNil(sample)
XCTAssertEqual(sample?.name, "Sample")
}
func decodeResource<T>(filename: String, type: T.Type) throws -> T? where T : Decodable {
guard let data = loadResource(filename: filename) else {
XCTFail("Failed to load JSON URL for \(filename)")
return nil
}
let resource = try JSONDecoder().decode(type.self, from: data)
return resource
}
func loadResource(filename: String) -> Data? {
guard let resourceURL = findResource(filename: filename) else {
return nil
}
let data = try? Data(contentsOf: resourceURL)
return data
}
func findResource(filename: String) -> URL? {
guard let resourcesURL = resourcesURL else {
return nil
}
let fileURL = resourcesURL.appendingPathComponent(filename)
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return nil
}
return fileURL
}
lazy var resourcesURL: URL? = {
guard let rootURL = findUp(filename: rootFilename) else {
return nil
}
return rootURL.appendingPathComponent(resourcesDir)
}()
func findUp(filename: String, baseURL: URL = URL(fileURLWithPath: #file).deletingLastPathComponent()) -> URL? {
let fileURL = baseURL.appendingPathComponent(filename)
if FileManager.default.fileExists(atPath: fileURL.path) {
return baseURL
} else {
return baseURL.pathComponents.count > 1
? findUp(filename: filename, baseURL: baseURL.deletingLastPathComponent())
: nil
}
}
}
{
"name": "Sample"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment