I recently migrated MyLargeProjectApp from CocoaPods to Swift Package Manager and noticed MyLargeProjectApp Tests suite test run phase went from 15 minutes to 46 minutes on CI — a 3.1x slowdown with no changes to the tests themselves.
MyLargeProjectApp has the following setup:
- MyLargeProject AppTarget
- MyLargeProject TestTarget (containing 11,000 tests)
It's a uniform overhead applied to every single test:
| Timing bucket | CocoaPods | SPM |
|---|---|---|
| < 0.1s | 86.6% of tests | 0.2% of tests |
| 0.1–0.5s | 11.0% | 94.0% |
The way the dependencies are linked with SPM is by having an a top-level umbrella SPM target AppApplication with all the dependencies for the app and test target. MyLargeProject AppTarget and MyLargeProject TestTarget both link AppApplication under Frameworks, Libraries, and Embedded Content.
The (MyLargeApp TestTarget ) is a hosted unit test that runs inside the app process.
import PackageDescription
// MARK: - Targets
let mainTargets: [Target] = [
.target(name: "Core"),
.target(name: "Networking", dependencies: ["Core"]),
.target(name: "UserData", dependencies: ["Core"]),
.target(name: "FeatureA", dependencies: ["Networking", "UserData"]),
.target(name: "FeatureB", dependencies: ["FeatureA"]),
.target(
name: "TestSupport",
dependencies: [
"Core",
.product(name: "Quick", package: "Quick"),
.product(name: "Nimble", package: "Nimble"),
]
),
// + 100 more
// Umbrella target — aggregates all feature targets for the main app
.target(
name: "AppApplication",
dependencies: ["Core", "Networking", "UserData", "FeatureA", "FeatureB", "TestSupport", /* + 100 more */]
),
]
// MARK: - Dynamic Framework Settings
enum Settings {
static let dynamicFrameworkTargets: [String] = ["AppApplication", "Core", "Networking", /*+ 9 more*/]
}
let package = Package(
name: "AppModules",
platforms: [.iOS(.v16)],
products: mainTargets.map { target in
Settings.dynamicFrameworkTargets.contains(target.name)
? .library(name: target.name, type: .dynamic, targets: [target.name])
: .library(name: target.name, targets: [target.name])
},
dependencies: [
.package(url: "https://github.com/quick/Quick", exact: "7.6.2"),
.package(url: "https://github.com/quick/Nimble", exact: "14.0.0"),
],
targets: mainTargets
)
AppApplication is declared as a dynamic framework in Package.swift.
Questions
- Is the per-test-class time increase due to dyld overhead?
- What could be causing this increase?
- Is CocoaPods somehow avoiding this overhead?
- How can I get the test time down
What I've Tried
- Removing
AppApplicationfrom test target: Link-time failure with ~100 undefined symbol errors