Two simple types using dependency injection.
struct CarKey {
let ownerInitial: String
let serialNumber: String
init(ownerInitial: String, serialNumberGenerator: SerialNumberGenerator = SecureAndExpensiveSerialNumberGenerator()) {
self.ownerInitial = ownerInitial
self.serialNumber = serialNumberGenerator.generate()
}
}
class SecureAndExpensiveSerialNumberGenerator: SerialNumberGenerator {
func generate() -> String {
// tested in SecureAndExpensiveSerialNumberGenerator, not desired to be called from elsewhere
}
}
In CarKeySpec
I can inject a test generator to prevent tests from needing the real SecureAndExpensiveSerialNumberGenerator
, while still making sure the serial number gets generated via the intended API. All good so far.
Let's expand our model layer with an additional type.
struct Car {
let owner: Person
let brand: Brand
let key: CarKey
init(owner: Person, brand: Brand) {
self.owner = owner
self.brand = brand
self.key = CarKey(ownerInitial: owner.name)
}
}
When testing this class (and any other class that isn't CarKey
), my goal is to absolutely avoid using SecureAndExpensiveSerialNumberGenerator
under the hood. (There are many classes I frequently want to avoid when running tests in iOS simulator. E.g. the keychain, NSUserDefaults
, NSURLSession
, NSDate
etc.)
Below is the best solution that comes to mind.
init(
owner: Person,
brand: Brand,
keyInitializer: (ownerInitial: String, serialNumberGenerator: SerialNumberGenerator) -> Key = Key.init(ownerInitial:serialNumberGenerator:)) {
self.owner = owner
self.brand = brand
self.key = keyInitializer(ownerInitial: owner.name, serialNumberGenerator: SecureAndExpensiveSerialNumberGenerator())
}
- the code is harder to understand – I'd argue testability hurts production code readability here (btw, this is a Swift 2.2 version with labeled selectors)
- we lost the entire power of default arguments by having to pass
SecureAndExpensiveSerialNumberGenerator()
from this layer again
If selector definitions could take default arguments into account (currently not available in Swift 2.2, not sure if possible), the latter point would go away:
keyInitializer: (ownerInitial: String) -> Key = Key.init(ownerInitial:)) {
This is definitely nicer, but can quickly get hairy again if your type has more than one property, so the first point still stands. When using Objective-C + Kiwi, I would stub Key.init
and make the initializer return whatever key I wanted to test with.
What is your usual approach in Swift?
Previous conversation here
They are the same thing, except that you'd keep the provider around to generate multiple instances. The difference is that you are discarding the provider (
keyInitializer
) after generating a single instance of Key. You'd be better off injecting that Key directly via the constructor.Now, that moves the problem of generating Keys somewhere else, yes. You want to push it as high in the dependency DAG as possible to reduce coupling. If we are taking about a static part of the DAG, i.e. you only have a single instance of Key in your dependency DAG, you'd move the init of that Key to the main() function/test.
Let's suppose you have a dynamic number of Keys, which seems to be the case. Seems that the Key is not the only dynamically-allocated object, seems like Car would be too (one car has a predefined number of Keys). You need to find the top-most object in the dep DAG responsible for allocating things dynamically, in this case it'd be a CarFactory (a CarFactory would create a dynamic number of Cars). You'd inject a provider of Keys and a provider of cars (or other parts, really) into this CarFactory, so it can assemble Cars.
Not sure what is your case but you either inject everything statically from the top if your dep DAG is static or you inject a provider, but not that deep in the hierarchy as in your example.
Hope it makes sense! 😅