Testing Tips & Tricks
Description: Testing is an essential tool to consistently verify your code works correctly, but often your code has dependencies that are out of your control. Discover techniques for making hard-to-test code testable on Apple platforms using XCTest. Learn a variety of tips for writing higher-quality tests that run fast and require less maintenance.
- The usual reminder about the “Pyramid of Tests”, three kinds of tests (from small to big, fast to slow):
- Unit: test specific functions/methods
- Integration: test classes/methods interaction
- End-To-End (System Tests, UI): test the user final experience
NotificationCenter
can use different environments (kind of likeUserDefaults
have different groups): you should avoid using the default notification center in your tests as this may affect other parts of the code (the test is therefore NOT isolated). To make things testable you should inject thenotificationCenter
in all your methods, going from this:
class Points0fInterestTableViewController {
var observer: AnyObject?
init() {
let name = CurrentLocationProvider.authChangedNotification
observer = NotificationCenter.default.addObserver(
forName: name,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleAuthChanged()
}
}
var didHandleNotification = false
func handleAuthChanged() {
didHandleNotification = true
}
}
To this:
class Points0fInterestTableViewController {
let notificationCenter: NotificationCenter
var observer: AnyObject?
init(notificationCenter: NotificationCenter) {
self.notificationCenter = notificationCenter
let name = CurrentLocationProvider.authChangedNotification
observer = notificationCenter.addObserver(
forName:name,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleAuthChanged()
}
}
var didHandleNotification = false
func handleAuthChanged() {
didHandleNotification = true
}
}
- You can also add a default value so you need to change the parameter only in the tests:
class PointsofInterestTableViewController {
let notificationCenter: NotificationCenter
var observer: AnyObject?
init(notificationCenter: NotificationCenter = .default) {
self.notificationCenter = notificationCenter
let name = CurrentLocationProvider.authChangedNotification
observer = notificationCenter.addObserver(
forName: name,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleAuthChanged()
}
}
var didHandleNotification = false
func handleAuthChanged() {
didHandleNotification = true
}
}
- When creating mocks, beside relying as much as possible on protocols, another neat thing your mock classes/struct should have is the possibility to overwrite some of their methods, so every test can change the behaviour of the mock accordingly on what you’re trying to test
- To make tests run even faster, you can try to skip some setups in your app launch by adding an environment variable or a launch argument in your schema:
And then use it in your app:
func application (_ application: UIApplication, didFinishLaunchingWithOptions opts: ...) -> Bool {
let isUnitTesting = ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1"
if isUnitTesting == false {
// Do UI-related setup, which can be skipped when testing
}
return true
}