In my UIViewController TDD screencast, I put IBOutlets and IBActions in the header file. This made them accessible to unit tests, but I knew it would raise questions:
@qcoding This seems like it complicates the question of whether or not actions and outlets truly belong in the public interface or not.
— Aengus McMillin (@aengusmcmillin) January 22, 2013
It’s a fair question. There’s a tension between information hiding (don’t reveal things in the interface) and testability (certain things need to be exposed). Exploring that tension leads me to apply the Extract Class refactoring in places I hadn’t considered before.
Objective-C classes don’t have anything that’s truly private, but it’s understood that if something isn’t published in the header file, hands off. As of Xcode 4, you can define outlets and actions in the implementation file, effectively hiding them. More hiding is good. But my screencast example works against it, what gives?
There is a way to have it both ways, a technique I used to use. I’ll show you how, then explain why I stopped using it.
How to hide (but still access for testability)
Let’s use the TDDCounter example from the screencast, but hide its outlets and actions. The interface shrinks:
@interface CounterViewController : UIViewController - (id)initWithCounter:(Counter *)counter; @end
The outlets move to a class extension in the .m file:
@interface CounterViewController () @property (weak, nonatomic) IBOutlet UILabel *countLabel; @property (weak, nonatomic) IBOutlet UIButton *plusButton; @property (weak, nonatomic) IBOutlet UIButton *minusButton; @end
That hides things. How do we re-expose them for the tests? By adding a category to CounterViewControllerTest.m:
@interface CounterViewController (Testing) @property (weak, nonatomic) IBOutlet UILabel *countLabel; @property (weak, nonatomic) IBOutlet UIButton *plusButton; @property (weak, nonatomic) IBOutlet UIButton *minusButton; - (IBAction)incrementCount:(id)sender; - (IBAction)decrementCount:(id)sender; @end
Tests pass—this is a successful refactoring. …So what’s the problem?
Why I avoid this technique
I once used the technique above. And if the thought of relaxing information hiding really bothers you, go for it. …But before you do, let me explain why I avoid it.
“Don’t Repeat Yourself” is one reason I’m no longer comfortable creating a testing category. Those repeated declarations? They make test maintenance a pain. They slow down refactoring, especially renaming.
Since enabling refactoring is a primary purpose of unit tests, slowing down refactoring is especially counter-productive. For me, it’s a bigger problem than exposing too much.
Encourages testing of privates
When I’ve used categories to make hidden outlets and actions accessible to tests, a curious thing happened: Other properties began sneaking in to join the outlets. Other methods began sneaking in to join the actions. The temptation is strong. Just use the word “testability” and it’s easy to justify!
With TDD, it’s sometimes hard to come up with a good test that’s small enough to achieve in a short time. But before you decide to TDD a hidden method, or verify it through a hidden property, pay attention. Another class is almost certainly trying to get out.
With legacy code, however, one adds unit tests after the fact. I may not feel comfortable extracting a class quite yet, because I don’t yet have the coverage I need for refactoring. So I’ll take the interim step of exposing certain methods and properties for testing. Their presence in the public interface is a reminder to me of unfinished work. And it’s easier to identify the class that wants to be extracted once enough previously hidden methods and properties are exposed.
OK, but what about outlets and actions?
For the most part, exposing IBOutlets and IBActions in the header file is a matter of expedience: testability trumps hiding. It would be easy to leave it at that, shrug, and move on.
But hold on. What if, as with legacy code, too many outlets and actions in the public interface is a reminder of unfinished work?
Imagine that the view I show in the screencast is more complex. Let’s say that it’s part of purchasing an item, and that the count determines the number of items to put into your shopping cart. So the view will also have item images, description, pricing, etc. Even if we extract the business logic, typical iOS code puts all the views together into one giant view controller.
Do you see the opportunity to Extract Class? We can pull out
minusButton to a subordinate view controller, separate from the other UI aspects of purchasing an item!
So when you write unit tests against a view controller (whether test-driven or test-after), don’t worry about hiding the IBOutlets and IBActions. Testability leads us to expose them, and exposing them may reveal that subsets of views we can group and extract. This is one way testing helps us discover better designs.
Question: What’s your favorite approach to exposing things for testability? Leave a comment below.