Teabyte

Mobile app development for iOS and Swift

Add test data to SPM packages

2021-05-15

At work we are slowly moving our whole app architecture towards using SPM packages to encapsulate different features of our apps. Since we write software, that is used in the medical environment, we need to write a lot of unit tests to verify that everything is working as intended. Therefore we often have test data in external files that are loaded during our tests. In this small article I want to highlight how you can include test data in test targets with the Swift Package manager.

Setup

For the purpose of this small article, let's assume we have the following directory structure of an SPM project called MyUtils.

.
├── Package.swift
├── README.md
├── Sources
│   └── <Source Files>
└── Tests
    ├─ MyUnitTest.swift
    ├── TestData
       └── TestData.txt
    └── Utils

Inside the Tests directory we created a folder called TestData which contains resources we want to load inside MyUnitTest.swift.

Declare data in Package.swift

SPM allows us to declare resource files inside the package manifest in a very convenient way. Resources can be added to .target as well as .testTarget declarations by adding the resources property. Its an array of "process rules" which tells SPM how to handle the provided resources. In total two rules are available:

  1. process - automatically applies optimizations to files. e.g. optimizing image files for different platforms
  2. copy - copies resources files at the given path without touching them. Directory structure is kept. You can read about the two rules here.

For adding test data, it is totally fine to use the copy rule as we do not need to optimize the files in any way for unit tests.

In the following you can find a default Package.swift file, which declares a library called MyUtils. MyUtils includes the MyUtils target which is tested by the MyUtilsTests target. MyUtilsTests includes every resource file inside the TestData directory.

Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
 
import PackageDescription
 
let package = Package(
    name: "MyUtils",
    platforms: [
        .iOS(.v14), .macOS(.v11)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "MyUtils",
            targets: ["MyUtils"]),
    ],
    dependencies: [],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "MyUtils",
            dependencies: [],
            path: "Sources" // Source files
        ),
        .testTarget(
            name: "MyUtilsTests",
            dependencies: ["MyUtils"],
            path: "Tests", // Test files
            resources: [
               .copy("TestData") // The test data files, copy files without modifying them
           ]
        )
    ]
)

Access the test data

After declaring our test data we can start using it. SPM automatically creates an accessor for your resource files. You can retrieve a path to a single resource by calling Bundle.module.path(forResource:ofType:). This method creates an optional path (String type) to the requested resource file.

MyUnitTest.swift
// Example error to indicate the test file could not be read successfully
enum ReadError {
    case fileNotFound
}
 
func testRead() throws {
 
    // Retrieve file path of test data and create URL out of it
    guard let filePath = Bundle.module.path(forResource: "TestData" ofType: "txt"),
          let url = URL(fileURLWithPath: file) else {
              throw ReadError.fileNotFound
          }
 
    // Read test data file contents
    let data = Data(contentsOf: url))
    let text = String(data: data, encoding: .utf8)
 
    // Check that reading of test data worked
    XCTAssertNotNil(text)
}

We can use the provided method to create a path to the resource we want to load. Afterwards convert it to an URL type and use it to read its content.

Conclusion

In this small article I showed you how you can include resource files in your tests when using the Swift Package Manager. If you have any questions or suggestions do not hesitate to reach out to me on Twitter.

Until next time and stay healthy 👋