Depend on nothing for your unit tests
Dependence injection (DI) is a useful technique to have in your unit testing toolbox. It's were we take implicit dependencies that our method/class rely on and make those dependencies explicit. It's especially useful when writing unit tests as it allows us to avoid using the black magic of method swizzling. A nice side effect of this is that you will create an application that is more decoupled, allowing you to have an easier time when refactoring.
Dependency Injection Forms
DI takes 3 main forms:
- Constructor/Initialiser Injection
- Property Injection
- Method Injection
As we will see below, you will have come across these 3 forms before - DI is a technique/pattern that developers stumble upon and use without knowing that they are.
Constructor/Initialiser Injection
Constructor/Initialiser Injection is were a dependency is passed in as a parameter during initialisation of an object. Take the following example of a class initialiser for the UTEAccount class (that holds the user, access token, sign-up state, etc):
- (instancetype)init
{
self = [super init];
if(self)
{
self.user = [UTEUser localUser]; //a class method to retrieve the locally signed-in user
}
return self;
}
In the above code snippet we can see that the initialiser is dependent upon (tightly coupled to) the UTEUser class. We can see that the UTEAccount class initialiser has to care about how the local user instance is created (which it shouldn't). In order to transform this dependency from an implicit one to an explicit and to remove the dependency on how the UTEUser instance is created we can pass the user in as a parameter.
- (instancetype)initWithUser:(UTEUser *)user
{
self = [super init];
if(self)
{
self.user = user;
}
return self;
}
With this code change UTEAccount no longer has to care about how UTEUser is created, just that it has UTEUser instance.
Property Injection
Property Injection is were a dependency is passed in as a property's value. So let's extend the UTEAccount class to handle what happens if a user hasn't signed-in yet. This means that we can't pass the UTEUser object in the initialiser (as it may not exist).
@property (nonatomic, strong) UTEUser *user;
This property would then be set when needed.
Method Injection
Method injection is very similar to Constructor/Initialiser Injection however it applies to all method in a class rather than just the initialiser.
Take the following example of a method:
- (void)configureAccountDetails
{
UTEUser *localUser = [UTEUser localUser];
self.labelDescription = [NSString stringWithFormat:@"The local user is: %@ with username: %@", localUser.fullName, localUser.username]];
}
In the above code snippet we can see that the method is dependent upon a UTEUser object. However if we were to alter our method so that it accepted an instance of UTEUser as a parameter it would break this implicit dependency.
- (void)configureAccountDetails:(UTEUser *)localUser
{
self.labelDescription = [NSString stringWithFormat:@"The local user is: %@ with username: %@", localUser.fullName, localUser.username]];
}
In the above snippet we have moved the 'UTEUser' instance from being an implicit dependency to an explicit dependency. Resulting a method that better defines it's purpose and making our life easier when writing unit tests for this method. We could break this down even more by passing the fullName and username as individual NSString parameters however I will leave that up to you as it's possible to go too far with this approach.
Of special note: the Singleton
A singleton is a global accessor for the properties that it contains, it works by allowing only one instance of the class to exist in the application's lifecycle. Usage of singletons are a primary candidate for the DI forms described above. Think of them as a code smell for DI.
Take the following example of a method:
- (NSString *)welcomeMessage
{
return [NSString stringWithFormat:@"Welcome to the %@ service", [UTEBrandManager sharedInstance].serviceName];
}
In the above code snippet like in all the others we see an implicit dependency which can be broken down by:
- (NSString *)welcomeMessageWithBrand:(NSString *)brand
{
return [NSString stringWithFormat:@"Welcome to the %@ service", brand];
}
Here we have used the Method Injection form to not only remove the singleton from the method body but even to remove any mention of it and instead use the standard Foundation frame class: NSString. This should lead to lead to more simple unit tests. In fact the more unit tests that you write the more powerful DI as a technique becomes.
More complexity
Now the above examples are all very simple but what if rather than one dependency we have 3, 4 or even 5 dependencies - at which point does the mess of a long list of properties/parameters become too much and we revert back to pre-DI code? The answer is that we don't revert, what has happened is that DI has shown us that the class/method is probably breaking the Single Responsibility Principle and should be broken down into smaller pieces. It is always better to expose any dependencies that we have as explicit and allow the next developer a better understanding of what they are dealing with, up-front then to hide these dependencies in the body of our class and force the developer to seek them out when changes to "unrelated" classes start to break other parts of the application.