One Way to Solve Inexplicable Race Conditions in Tests
Today I wrote my first asynchronous test in my latest project and suddenly, during the waitForExpectations
run loop, a totally unrelated test’s stuff crashed. To make things worse, it was part of an RxSwift subscription. No idea who kept that alive and where the EXC_BAD_ACCESS
really originated.
It helped to make sure that no object references are kept around longer than necessary. In unit tests, this can mean to make your test doubles and collaborating objects optionals:
class BananaTests: XCTestCase {
var banana: Banana!
var tree: Tree!
var monkey: Monkey!
override func setUp() {
super.setUp()
tree = Tree()
monkey = Monkey()
banana = Banana(on: tree, eatenBy: monkey)
}
override func tearDown() {
tree = nil
monkey = nil
banana = nil
super.tearDown()
}
}
If you just go the lazy route and make the objects properties of the BananaTests
class, the objects will be kept around until all tests have finished and the BananaTests
instance is being disposed of. With the setup–tear down dance here, we manage to get rid of references right after each test execution.
In case you wonder what the lazy version looks like where the latest references are kept alive:
class BananaTests: XCTestCase {
var banana: Banana!
let tree = Tree() // <- uh oh
let monkey = Monkey() // <- guess who stays around ...
override func setUp() {
super.setUp()
banana = Banana(on: tree, eatenBy: monkey)
}
}
I’d love to tell you now that this solved my problem. (It didn’t.) Instead, I want you to keep this in mind as a potential source of problems. Tests for NotificationCenter
subscribers will are a great example of problematic tests: the subscribers will continue to receive notifications when other test suites run.