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
Improve your test writing “Flow.”
Sign up to get my test-oriented code snippets.
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 countLabel, plusButton, and 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.
I have come across the same dilemma as you mention. My (temporary) solution is to extract the “private” properties (that is, properties defined in the .m file) to a “private” file. In relation to your example, I would create a CounterViewController_Private.h in which I would define my “private” properties in a class extension of CounterViewController.
This isn’t ideal either, I am aware of that. But it achieves testability of the “private” properties and it eliminates the maintenance issue, since changing something in the *_Private.h file will take effect in both the source and the test files.
Kasper, that’s not a bad approach.
I agree, but just a few minutes ago, I did the opposite. The justification for my decision was that it was just one method, generating a somewhat complicated URL, used only by this class.
I sat down and wrote a comment explaining why I did it. When I was ready to post the comment, I realized that the URL generation had really nothing to do with the main purpose of the class under test. I’m now going to extract the method into a new class.
Thanks for the reminder:-)
Excellent, André. I’m actually going back over some of my cleanest code with new eyes, discovering many places for Extract Class I missed before.
I think that outlets and actions are public interface anyway. They’re methods and properties that are used by other objects; the fact that those objects may be created in a non-code tool doesn’t change that fact.
Nice insight, Graham.
Hey the rest of you: if you don’t have a copy of Graham’s book on iOS TDD, what are you waiting for?
I’ve always been very strict about not exposing actions in the interface, but when I started TDD, I indeed struggled with Testability vs hiding concept.
I tried the (Test) category exposing private fields as well but thought that was a lot of code to maintain.
Your statement is very insightful and I had never though of it that way!
You just made my life easier :)
I’m glad, Sacha! Identify those smaller classes and extract :)
Please, check my question on Stack Overflow:
I’ve stumbled across similar problem, only, in my situation, views created inside view controller.
I posted my answer on Stack Overflow.
There’s always an option to use KVC to access those outlets from outside,
Thus you can test almost everything without duplicating code in categories or exposing private members on public interface.
Hmm, maybe. But it feels… sneaky to me.
I guess I have fewer qualms about hiding. But for folks who are more squeamish about not hiding things, you may have a good solution.
I like the idea of keeping iboutlets in separate private file because I have too many outlets in h file and it looks really bad. Excellent answer I found on Stackoverflow.