Speeding up your iOS Swift tests
22 Jan 2019Are your iOS Swift isolation tests running fast enough? My guess would be “no” unless you have taken deliberate actions to make them run faster. Here’s an approach to reduce the time we spend waiting for our tests to run.
Problem
If I head to Xcode and create a new iOS project with File > Project > Single View App
. This gives me a bare bones project which hasn’t yet been sullied with my attempt at coding. I select the option to include unit tests so that everything is wired up and I get a blank test class.
From here I run the tests to get a sense of what my feedback cycle will be:
I’m not going to dig into why these run times are soo slow but just know that this is way off the mark. This is a project with no production code and an empty test file but I have to wait ~30 seconds for feedback.
The End Result
Before going through the practical details of the Swift Package Manager based approach to speeding this up let’s look at the end results - this way I can avoid wasting people’s time if they decide the improvements don’t merit reading further.
Here I look at a single module extracted from a production code base. It’s a small module at 4633 lines of Swift, which is made from several smaller modules. It’s executing a modest 110 tests:
Xcode Clean | SPM Clean | Diff |
---|---|---|
46.873 | 22.717 | 2.06x faster |
54.753 | 19.897 | 2.75x faster |
58.447 | 19.883 | 2.93x faster |
Clean builds are still painfully slow but they are 2-3x faster when using this technique so that’s still a win.
If you simply rerun the tests without changing any code you get a wild speed up of ~39% but that’s not a realistic test case. So the numbers for cached builds here will take into consideration changing some code to force some recompilation:
Xcode Cached | SPM Cached | Diff |
---|---|---|
31.840 | 7.389 | 4.30x faster |
29.501 | 7.166 | 4.11x faster |
25.396 | 6.306 | 4.02x faster |
This is where the numbers get more exciting - ~7 seconds is still an awfully long time to wait for feedback when you are in a flow but it’s much more manageable than 30-60 seconds.
How?
So you made it past the results - let’s dig in. I’ve actually discussed this general idea already in Creating Swift Package Manager tools from your existing codebase (one of my catchier titles) but this is applying it to improving tests.
Prerequisites
- If your code is well organised in folders then this will be easier
- The code you want to test can’t
import UIKit
- Ideally you should avoid
import UIKit
as much as you can as this code will be more portable
- Ideally you should avoid
Steps
- In your project directory create a new Swift Package Manager project
- Symlink your production code into the new project’s
Sources
folder - Symlink your test code into the new project’s
Tests
folder - Update the
Package.swift
file to reference your new module - Run
swift test
Worked example of a simple project (not modularised)
Imagine I have a project called MySimpleApp
, which has all the business logic separated from the UIKit
related stuff. On disk it looks like:
├── MySimpleApp
│ ├── AppDelegate.swift
│ ├── Models <- business logic in this folder
│ │ ├── BlogPost.swift
│ │ └── BlogFilter.swift
│ └── ViewController.swift
├── MySimpleApp.xcodeproj
└── MySimpleAppTests
└── Models
├── BlogPostTests.swift
└── BlogFilterTests.swift
I would start by creating a new SPM project - I’m going to go with the arbitrary name of UIKitless
. The swift package init
command could be used but it generates a lot of stuff we don’t need so it’s simpler to build it manually here:
Next we need to create a Package.swift
to tell SPM how to build all of this. The full file will look like this:
With this we can run swift test
and everything should work.
Worked example of a more complicated project
If you have modularised your project in any way then it will mean that you are using statements like import SomeOtherModule
in your source files. This is not a problem as long as the module you want to import also follows the prerequisites above.
Here’s the additional steps that are required:
- Symlink in the other module’s sources
- Update the
Package.swift
file to tell SPM about this module
The trick is to use the same module name in SPM as is used in your code base e.g. if I import SomeOtherModule
in my source files then the target name of my symlink should be SomeOtherModule
The last thing is to update Package.swift
to tell it to build this new module and make it a dependency of MySimpleApp
:
With this we can run swift test
and everything should work.
Conclusion
Whilst this example is only using a small code base and possibly unrealistic tests it shows some promise. Hopefully I’ve shown that depending on how your codebase is structured this could be quite a cheap experiment to run and if it yields good results then you can keep using it. Having done a fair amount of Ruby where I would expect my tests to run in fractions of a second any time I save a file these tests are still mind numbingly slow - these things are improving all the time so I remain optimistic that testing won’t be this painful forever.