- Question:
- Form of dependency injection (DI)
- Constructor Injection
- Property Injection
- Method Injection
- Ambient Context
- Extract and Override Call
- “So which method should I use?”
- Constructor Injection
- Property Injection
- Method Injection
- Ambient Context
- Extract and Override Call
- FAQ
- “Which framework should I use?”
- “I don’t want to expose all the hooks.”
- DI greater than test
Question:
Take a look at the code:
- (NSNumber *)nextReminderId {
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
// Increment the last reminderId
currentReminderId = @([currentReminderId intValue] + 1);
} else {
// Set to 0 if it doesn't already exist
currentReminderId = @0;
}
// Update currentReminderId to model
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
Copy the code
How do YOU test this code? The problem is that this method uses NSUserDefaults, which is out of our control.
This isn’t just a discussion of “How do I test a method that uses NSUserDefaults?” , but rather, “How can we test a method if it depends on another object that reduces the rapidity and repeatability of testing?”
One of the biggest obstacles when you start writing single tests is not knowing how to manage the dependencies of the methods under test, but there are a number of ways around this, called dependency Injection (DI).
Form of dependency injection (DI)
For DI, one would initially think of a dependency injection framework or an Inversion of Control (IoC) container, but this discussion is slightly different.
There are various techniques for capturing dependencies and injecting additional content. For objective-C Runtime, the Swizzling technique does just that. At this point, the rest of the discussion will feel unnecessary. But we still want code dependencies to be explicit, so we can see dependencies (when dependencies are too many or wrong, we have to deal with the “bad smell” of code).
So let’s take a quick look at some forms of DI.
Constructor Injection
Objective-c does not have a Constructor itself. The more familiar term is the initialization method. Constructor Injection is used here because it is a standard DI term that is easier to understand across languages.
For constructor injection, the dependency is passed in as an argument to the constructor and stored internally for subsequent use:
@interface Example(a)
@property (nonatomic.strong.readonly) NSUserDefaults *userDefaults;
@end
@implementation Example
- (instancetype)initWithUserDefaults:(NSUserDefaults *userDefaults)
{
self = [super init];
if (self) {
_userDefaults = userDefaults;
}
return self;
}
@end
Copy the code
Dependencies can be captured in instance variables or properties. The above example uses read-only attributes to make it harder to tamper with.
Injecting NSUserDefaults might seem strange, and that’s exactly what this example doesn’t do. Remember that NSUserDefaults represents a dependency that can cause trouble. The injected value should be an abstraction (that is, an ID that satisfies a protocol) rather than a concrete object. But I’m not going to discuss that in this article; Let’s continue using NSUserDefaults as an example.
Now, every reference to singleton [NSUserDefaults standardUserDefaults] in this class should refer to self.userDefaults:
- (NSNumber *)nextReminderId {
NSNumber *currentReminderId = [self.userDefaults objectForKey:@"currentReminderId"];
if (currentReminderId) {
currentReminderId = @([currentReminderId intValue] + 1);
} else {
currentReminderId = @0;
}
[self.userDefaults setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
Copy the code
Property Injection
In property injection, the code for nextReminderId looks the same, referring to self.userdefaults. But instead of passing the dependency to the initializer, we set it as a settable property:
@interface Example
@property (nonatomic.strong) NSUserDefaults *userDefaults;
- (NSNumber *)nextReminderId;
@end
Copy the code
Now, a test can construct this object and then set the userDefaults property as needed. But what happens if the property is not set? In this case, let’s use lazy initialization to establish a reasonable default value in the getter:
- (NSUserDefaults *)userDefaults {
if(! _userDefaults) { _userDefaults = [NSUserDefaults standardUserDefaults];
}
return _userDefaults;
}
Copy the code
Now, if any calling code sets the userDefaults property before use, self.userDefaults will use the given value. But if the property is not set, then self.userDefaults will use [NSUserDefaults standardUserDefaults].
Method Injection
If the dependency is only referenced in a single method, then we can inject it directly as a method parameter:
- (NSNumber *)nextReminderIdWithUserDefaults:(NSUserDefaults *)userDefaults {
NSNumber *currentReminderId = [userDefaults objectForKey:@"currentReminderId"];
if (currentReminderId) {
currentReminderId = @([currentReminderId intValue] + 1);
} else {
currentReminderId = @0;
}
[userDefaults setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
Copy the code
Again, this may seem strange – and keep in mind that NSUserDefaults may not be perfect for every example. But NSDate arguments and method injection are a good fit. (More on this below when we discuss the benefits of each form.)
Ambient Context
When a dependency is accessed through a class method (such as a Singleton), there are two ways to control the dependency through tests:
- If you control a singleton, you can expose its properties to control its state.
- If fiddling with properties is not enough, or if singletons are beyond your control, then it’s time to change: replace the class method so that it returns the dummy method you need.
I won’t go into the details of a dizzying example; There are many other resources available in this area. But see? Swizzling can be used for DI. But read on. After a brief overview of the different forms of DI, we’ll discuss their strengths and weaknesses.
Extract and Override Call
This last technique does not belong to the form of DI in Seemann’s book. Instead, the extract and Override calls come from legacy code that effectively handles Michael Feathers. Here’s how to apply this technique to our NSUserDefaults problem in three steps:
- Select one of the calls
[NSUserDefaults standardUserDefaults]
. Use Automated Refactoring (in Xcode or AppCode) to extract this into the new method.
- Change the other location where the call is made to replace it with a call to the new method. (Be careful not to change the new method itself.)
The modified code looks like this:
- (NSNumber *)nextReminderId {
NSNumber *currentReminderId = [[self userDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
currentReminderId = @([currentReminderId intValue] + 1);
} else {
currentReminderId = @0;
}
[[self userDefaults] setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
- (NSUserDefaults *)userDefaults {
return [NSUserDefaults standardUserDefaults];
}
Copy the code
Once you are ready, the final step is:
- Create a special test subclass that overrides the extraction method as follows:
@interface TestingExample : Example
@end
@implementation TestingExample
- (NSUserDefaults *)userDefaults {
// Do whatever you want!
}
@end
Copy the code
The test code can now instantiate TestingExample instead of Example, and has full control over what happens when the production code calls [self userDefaults].
“So which method should I use?”
We have five different forms of DI. Each has its own strengths and weaknesses, so each has its place.
Constructor Injection
Constructor injection should be your weapon of choice. If in doubt, start here. The advantage is that it makes dependencies explicit.
The downside is that it feels cumbersome at first. This is especially true when initializers have a long list of dependencies. But this reveals a previously hidden code smell: Do classes have too many dependencies? Perhaps this is not consistent with the principle of single responsibility.
Property Injection
The advantage of property injection is that it separates initialization from injection, which is necessary when you cannot change the caller. The downside is that it separates initialization from injection! This makes incomplete initialization possible. This is why it is best to use a dependency when it has a specific default value, or when you know the dependency will be populated by the DI framework.
Property injection looks simple, but making it robust is tricky:
You may need to prevent properties from being reset arbitrarily. Therefore, you might need a custom setter instead of the default setter to ensure that the instance variable is returned nil and the given argument is non-nil.
Do getters need to be thread safe? If so, it would be easier to use constructor injection instead of trying to make getters both safe and fast.
Also, be careful not to automatically favor property injection just because a particular instance comes to mind. Ensure that the default values do not reference other libraries. Otherwise, you will require users of your classes to include other libraries as well, breaking the benefits of loose coupling. (In Seemann’s terms, this is the difference between a local default and an external default.)
Method Injection
Method injection is good when dependencies change from call to call. This could be the application-specific context of the call point. It could be random numbers. It could be the current time.
Consider ways to use the current time. Try adding the NSDate argument to the method instead of calling [NSDate date] directly. With a small increase in invocation complexity, it provides some options for using the method more flexibly.
(While Objective-C makes it easy to replace test doubles without the need for a protocol, I recommend reading J.B.Rainsberger’s “Beyond Mock Objects.” This is an interesting example of how to fight injection dates, which opens up larger design and reuse issues.)
Ambient Context
If you have a dependency that is used at various low-level points, you may have a cross-domain problem. Passing this dependency on at a higher level can interfere with your code, especially if you can’t predict in advance where it will be needed. Such as:
- In the login
[NSUserDefaults standardUserDefaults]
[NSDate date]
Your surroundings may be just what you need. But because it affects the global context, don’t forget to reset it when you’re done. For example, if you swizzle a method, use tearDown or afterEach (depending on your test framework) to restore the original method.
Instead of making your own swizzling, see if someone has written a library and pay attention to the surroundings you need. Such as:
- Networking – OHHTTPStubs
- NSDate – TUDelorean
Extract and Override Call
Because the extract and Override calls are so simple and powerful, you might want to use them anywhere. But because it requires test-specific subclasses, tests can easily become vulnerable.
That said, it works great for legacy code, especially if you don’t want to change all callers.
FAQ
“Which framework should I use?”
My advice to anyone just starting out with mock objects is not to use any mock object frameworks at first, as you will have a better understanding of what is happening. My advice to anyone starting out with DI is the same. But you can take DI one step further without a framework, relying entirely on “Poor Man’s DI,” and you can do it yourself.
In fact, there’s a good chance you’re already using the DI framework! It’s called Interface Builder. IB is not just about layout interfaces; By declaring these properties as IBOutlets, you can populate any property with real objects. This is useful for creating object diagrams when creating views. In his 2009 article “The Dependency Inversion Principle and the iPhone,” Eric Smith called Interface Builder “my favorite DI framework” and gave an example of how to use Interface Builder for dependency injection.
If you decide that you need a DI framework, Interface Builder is not enough, how do you choose a good one? My advice: be wary of any framework that requires code changes. Once you have to subclass an object, comply with a protocol, or add some kind of annotation, you can bind your code directly to a particular implementation. (This runs counter to the basic idea behind DI!) Instead, find a framework that lets you specify connections from outside the class, either through a DSL or in code.
“I don’t want to expose all the hooks.”
Exposing injection points in initializers, properties, and method parameters can make it look like you’re breaking encapsulation. People want to avoid showing seams, because it’s easy to tell yourself that seams exist only to support testing and therefore are not part of the API. This can be done by declaring them in categories in a separate header file. For Example, if we are dealing with example.h, create an additional header exampleInternal.h. This will be imported only by example.m and the test code.
But before you take this approach, I want to challenge the idea that DI leads to broken encapsulation. What we are doing is making dependencies explicit. We are defining the edges of the components and how they fit together. For example, if a class has an initializer whose parameter type is ID
, then it is obvious that in order to use the class, it needs to be given an object that satisfies the Foo protocol. You can think of it as defining a set of sockets on a class, along with the Plug-ins that match those sockets.
When exposing dependencies feels cumbersome, see if these scenarios are appropriate:
-
Does exposing your dependency on Apple objects feel foolish? Is anything apple offers implicit? So is it fair for any code? Not necessarily. Take our NSUserDefaults: What if you decided to avoid using NSUserDefaults for some reason? Explicitly identifying it as a dependency rather than hiding it as an implementation detail will remind you to investigate the component. You can check if the use of NSUserDefaults violates design constraints.
-
Do you feel like you have to expose a bunch of internal interfaces in order to test your classes? First, see if you can write tests that only work through existing public apis (and still be fast and deterministic). If you can’t, if you need to manipulate dependencies that would otherwise be hidden, it’s possible that another class is trying to get rid of them. Extract it, convert it to a dependency, and then test it separately.
DI greater than test
My initial motivation to explore DI came from test-driven Development, because in TDD you often get the question “How do I write unit tests for this?” “But I found that DI was actually concerned with a bigger idea: that our code should be made up of modules that we could splice together to build an application.
This approach has many benefits. Graham Lee’s article “Dependency Injection, iOS and You” describes some of these: “To adapt… New requirements, bug fixes, new features, and testing components individually.”
So keep that more important idea in mind when you start writing unit tests with DI. Place the pluggable module behind your head. It will inform many design decisions and guide you to learn more about DI patterns and principles.