Xcode unit tests with ⌘+S
Practicing TDD using Xcode IDE can be a bit disruptive. Every time you introduce a small change in the implementation or test file, Xcode rebuilds entire project (fortunately using incremental build) and installs the app on a simulator. Depending which project configuration you have, it may take from a few to dozens of seconds. This article presents a way to bypass this long lasting procedure and automatically run unit test(s) whenever you save your implementation
.swift file. We will leverage new feature of John Holdsworth’s “Injection for Xcode” app — support for auto-testing.
InjectionTDD (together with InjectionForXcode) runs unit tests on a simulator environment without rebuilding nor reinstalling the app. Implementation and tests are injected into already-running host app, thus work almost in a real-time within a standard, well-known Xcode IDE. In addition, to speed-up testing process, InjectionTDD runs only a subset of test cases — these related to your just-updated implementation file.
It is crucial to get quick feedback whether your tests are passing (green) or failing (red) if you are making TDD, but not only then. Wouldn’t be nice to instantly know that your change/fix does not introduce any regression somewhere else? Or maybe you are covering your implementation with new test cases and it becomes frustrating to rebuild test target all over again? If you observe the same issues, InjectionTDD can save you a lot of time!
Normally, to run unit test you have to wait for several steps like: building, linking, installing entire app and connecting Xcode with lldb server. Depending on your configuration it may take up to dozens of seconds. However, during development we quite often modify only one particular file and then observe the results. InjectionForXcode saves a ton of time by compiling updated file on-fly and replacing/swizzling its implementation in a live iOS process. Very similar approach can be applied for unit testing. This is how InjectionForXcode+
InjectionTDD works in general:
First, Xcode builds your test target, installs an app on a simulator and tries to perform first tests. However, dedicated framework called
InjectionTDD, that you have to integrate into a test target, stalls test executions and keeps a runtime in a constant waiting loop.
Then, whenever you change something in Xcode, InjectionForXcode builds a slice of an application (only edited file + related unit tests) and injects it into already waiting “Hosting app”. At last, host app executes all tests and passes results back to the Xcode to presents them in its standard UI. Keep in mind that host app is not terminated afterwards and the entire process can be repeated all over again.
Armed with theory background, let’s roll our sleeves up and play with it. You can start with any existing project with unit tests that is written in Swift or just try it out by pods command
pod try InjectionTDD:
- Before any work, ensure that terminal command
xcode-select -ppoints to the current Xcode version path. If not, assign correct version using
sudo xcode-select -s /Applications/YOUR_XCODE.app/Contents/Developer
- Install free “Injection for Xcode” app (remember to install version with TDD support) from official site and run it. Note that application sits only in the tray.
InjectionTDDframework to your test target (CocoaPods, Carthage and manual installations are supported). If you use CocoaPods, it’s a single line change:
target 'YourAppTests' do
pod 'InjectionTDD', '~> 0.5'
- Start your unit tests (⌘+U) on a simulator. After a while, Xcode console should print confirmation
Connected to “Injection” plugin, ready to load x86_64 code:
- We are ready to edit and save any of your implementation
.swiftfile and click on “Inject Source” option in the Injection menu tray (or use shortcut ⌃=).
InjectionForXcode starts magic and after a while you should see tests results in the console output and Test navigator (⌘+6) 🎉.
- Optionally: you can enable “File Watcher” in the tray menu so that injection happens automatically, whenever you save a file. This is really handy.
- Optionally: by default test results are printed to the console, but if you want to also present notification summary (as below), install custom Xcode breakpoints to achieve the same result (see manual):
How does it work under the hood?
If you are interested in the implementation details, here are four stages that happen whenever you introduce a change to the implementation file:
- lookup of all test target files to find tests cases that directly call code from the implementation file: uses Swift’s metadata files
.swiftdepsdescribes which Swift symbols given file provides and depends on),
- compile your
swiftproject target using incremental build (commands are parsed from Xcode logs) and bundle it into a
- send just compiled binary to the simulator process: iOS simulator app loads from a disk dynamic framework,
- execute all
XCTestCases on a simulator.
One tool fits all
Contrary to Objective-C, which depends on message dispatching, Swift uses also static dispatch, what makes runtime method swizzling impossible. However, for
InjectionTDD is not a case, where implementation and unit tests are bound together in a linking process so that unit tests always point to up-to-date implementation. In other words,
XCTestCases are tightly coupled (by the time of compilation) with a unique type that is under test.
Therefore, you can test all kinds of Swift types with all dispatching mechanisms:
classes, final classes,
InjectionTDD framework stalls execution of all tests and waits for injected tests. This is intended during development, but on a CI machine we want to normally execute all tests and never expect any injected tests. If you are afraid that integrating
InjectionTDD framework only locally is an overkill, there is a remedy. You can control test execution (whether run them normally or wait for injection) by special flag in a scheme environment variable called
INJECTION_TDD_SKIP. If you set it to TRUE, all your unit tests will execute as normal:
Therefore, it is recommended to keep at least two schemes committed to a repo:
- Used for building, where
- TDD development only (disabled
Your CI building script always use the first scheme, developers may switch to the latter one while coding.
Hint: to speed up entire injection process, development TDD scheme (2.) shouldn’t gather Code coverage. It can cut off couple of precious seconds.
“Injection for Xcode” is a powerful and popular tool for live-updating your application in a runtime, especially when mastering UI details. Nowadays, it supports also unit testing in real-time that saves a lot of time and provides great integration with Xcode IDE.