Hi, first post here
A colleague and I have run into an interesting scenario and I've not yet found resources to help explain what is happening.
For any struct that has at least one defaulted property, the behaviour is observably different between using the generated memberwise init and a seemingly identical explicit init (both using internal access levels – I'm not opening that can of worms).
What seems to be happening is that those properties that have default values are assigned their default values before entering our explicit init. To me, this doesn't make sense, but I'd like to learn why
struct ThisWorks {
var number: Int = { fatalError("ThisWorks::'default' number loaded! 😳") }()
}
struct ThisCrashes {
var number: Int = { fatalError("ThisCrashes::'default' number loaded! 😳") }()
init() {}
init(number: Int) {
self.number = number
}
}
// These two obviously invoke the code with the fatalError
//_ = ThisWorks()
//_ = ThisCrashes()
// These two are more interesting. Externally they seem identical,
// but behave very differently
_ = ThisWorks.init(number: 5) // Works ✅ default value not loaded
_ = ThisCrashes.init(number: 5) // Crashes 💥 "Fatal error: ThisCrashes::'default' number loaded! 😳"
In most cases, this wouldn't be an issue – you rarely have properties whose default value is (or causes) a "fatal error".
In our case however, some default values may be injected from a different module and, when running unit tests, that module may not be connected*, prompting a fatal error.
*when unit testing, we want to mock/stub all dependencies, including the existence of other modules or external libraries
A watered-down real life example
In our Business module:
class GetCurrentUser {
private let dependencies: Dependencies
struct Dependencies {
var storage: UserStorageProtocol = ServiceLocator.userStorage
}
init(dependencies: Dependencies = .init()) { /* ... */ }
func execute() -> User? {
return dependencies.storage.currentUser
}
}
enum ServiceLocator {
static var userStorage: UserStorageProtocol! // To be set when app launches
}
class GetCurrentUserTests: XCTestCase {
func testFoo() {
let dependencies = GetCurrentUser.Dependencies(storage: /* some mocked storage */)
let sut = GetCurrentUser(dependencies: dependencies)
// Carry on with test
}
}
An implementation conforming to UserStorageProtocol
is defined in a different module, "Storages". The Storage module is not a dependency of the Business module (i.e. framework is not linked) but is injected by the containing app target at run time.
When running unit tests for the Business module, the app is not (and should not be) launched so therefore ServiceLocator
is not set up (i.e. ServiceLocator.userStorage
is nil
). Accessing it within unit tests is a programmer error.