Use Resources in Unit Tests with Swift Package Manager

Use resources in unit tests with Swift Package Manager

Swift 5.3

See Apple Documentation: "Bundling Resources with a Swift Package"

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.

// 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: "Example",
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 file in Sources/Example/Resources/*
.process("Resources"),
]),
.testTarget(
name: "ExampleTests",
dependencies: [Example],
resources: [
// Copy Tests/ExampleTests/Resources directories as-is.
// Use to retain directory structure.
// Will be at top level in bundle.
.copy("Resources"),
]),

Reported Issues & Possible Workarounds

  • 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 Xcode project, add an Xcode build phase copy to put the Resource into some *.bundle directory, and add a some custom #ifdef XCODE_BUILD compiler directive for the Xcode build to work with the resources.

#if XCODE_BUILD
extension Foundation.Bundle {

/// Returns resource bundle as a `Bundle`.
/// Requires Xcode copy phase to locate files into `ExecutableName.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("Foundation.Bundle.module could not load resource bundle: \(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

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

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.

Write unit test in swift package manager for API call

It's really good you're getting to know unit tests (they'll make your life SOOOO much easier, trust me).

A few things I must mention about your snippet of code above, since you're calling Alamofire directly in your API core class (AF.request) you won't be able to mock your network and hence not being able to perform a unit test per se. Sure, you could either:

  • Use an HTTP proxy for your project to interact with and hence make your test reliable but that would introduce a whole lot of boilerplate and set up.
  • Interact directly with a test/mock endpoint but this also comes with a caveat since you'd be relying on your network connection and having the need to introduce a bunch of waiting time in order to be able to sync your tests with the response times. Not recommendable.

You should check these resources out:

  • Unit testing your network layer
  • My own network library in SPM

I need to improve the documentation on my library but the network layer is fully covered in tests + it's SPM (after all that was your original question).

I wish you all the best of lucks with automated testing in general, cheers!

PS: You should consider dismiss network layer dependencies for simple tasks. Libraries such as Alamofire and Moya are great choices if you need to do some really heavy lifting on the network side, otherwise they are unnecessary boilerplate. Check out these other great resources if you need a starting point:

  • Networking In Swift With URLSession: nice intro to dip your toe in Swift's native URLSession waters
  • Network Requests and REST APIs in iOS with Swift (Protocol-Oriented Approach): more advance stuff if you already are familiar with protocols and generics.

How to expose CoreData to swift package unit tests?

If you declare a Swift tools version of 5.3 or later in your package manifest, you can bundle resources with your source code as Swift packages. For example, Swift packages can contain asset catalogs, storyboards, and so on.

When resources are defined, a new static Bundle reference is created for the package. This can be accessed using Bundle.module.

So for your ManagedObjectModel you will need update the bundle reference. A good way of using this is to have an accessor in your package that will return the model.

For additional information, you can check out Apple's developer documentation Bundling Resources with a Swift Package.

Copying Resource Files For Xcode SPM Tests

This is another workaround to provide access to test resources. Hopefully an answer to the OP's question will be forthcoming.

Using the code below, an extension is created to allow callers to create URL's to test resources like this.

let url = URL(forResource: "payload", type: "json")

This code requires that all resource files be located in a flat directory named "Resources" just under the test target.

// MARK: - ./Resources/ Workaround
// URL of the directory containing non-code, test resource fi;es.
//
// It is required that a directory named "Resources" be contained immediately below the test target.
// Root
// Package.swift
// Tests
// (target)
// Resources
//
fileprivate let _resources: URL = {
func packageRoot(of file: String) -> URL? {
func isPackageRoot(_ url: URL) -> Bool {
let filename = url.appendingPathComponent("Package.swift", isDirectory: false)
return FileManager.default.fileExists(atPath: filename.path)
}

var url = URL(fileURLWithPath: file, isDirectory: false)
repeat {
url = url.deletingLastPathComponent()
if url.pathComponents.count <= 1 {
return nil
}
} while !isPackageRoot(url)
return url
}

guard let root = packageRoot(of: #file) else {
fatalError("\(#file) must be contained in a Swift Package Manager project.")
}
let fileComponents = URL(fileURLWithPath: #file, isDirectory: false).pathComponents
let rootComponenets = root.pathComponents
let trailingComponents = Array(fileComponents.dropFirst(rootComponenets.count))
let resourceComponents = rootComponenets + trailingComponents[0...1] + ["Resources"]
return URL(fileURLWithPath: resourceComponents.joined(separator: "/"), isDirectory: true)
}()

extension URL {
init(forResource name: String, type: String) {
let url = _resources.appendingPathComponent("\(name).\(type)", isDirectory: false)
self = url
}
}

Swift package unit testing with CoreData

According to this post, .xcdatamodeld files aren't supported because you can't bundle resources, so if you want to use Core Data models you have to do it programmatically (shown how to in the post).

P.S. I'm in the process of figuring this out myself too and I think the other thing to keep in mind will be specifying managedObjectModel (in addition to name) if/when instantiating NSPersistentContainer.



Related Topics



Leave a reply



Submit