Let’s look at how a change to unit testing empowers TDD.
[This post is part of the series TDD Sample App: The Complete Collection …So Far]
One way to get unit tests wrong is to make them more rigid than they need to be. In the MarvelBrowser iOS TDD sample app, we want to fetch comic book characters from the Marvel service. We’ll do this by converting a fetch characters request model into a URL request. To fetch characters with:
we want to generate a URL with a query string. Something like:
Here’s the catch for unit testing: I said “something like”, not “exactly like”.
A naive TDD approach says, “testing for equality is easy, so do that.” But this isn’t helpful for complex results. In this example,
What we really need to do is parse the result. Of course, that’s more complex than the linear given/when/then of a unit test. We need a test helper that is itself fully tested. I described various approaches in How to Avoid Problems with Assertion Test Helpers.
Watch what happens when I take time to prepare a custom matcher. In this 7½-minute screencast, I quickly TDD the URL path, then begin to generate the query string.
When I inlined the dummyRequestModel at 3:25, I used AppCode’s “Refactor This” pop-up menu, which I invoked with Control-T. Typing anything filters the options. In the screencast, I got as far as “inl” to make “Inline…” the only option.
I like to lean on the “Refactor This” menu; it’s so convenient! Then I begin learning the keyboard shortcuts for options I use all the time, like Extract Variable.
I did use a shortcut in my TDD. At 4:22 when I have a failing test
Expected URL with "nameStartsWith" = "NAME", but no query in <https://gateway.marvel.com/v1/public/characters>
strict TDD suggests that I should hard-code the query string
?nameStartsWith=NAME. Maybe I should have. But I took the shortcut of jumping ahead to a parameterized solution without a second test to drive this need.
My TDD of the percent-encoding also took a shortcut which leaves a potential hole. At 6:15 when I use URLQueryAllowedCharacterSet, there’s nothing driving the use of that particular NSCharacterSet over any other.
Oh well. I’d rather have imperfect TDD than no TDD. If enforcing the character set becomes a necessity, we can always add unit tests after the fact.
The 3 steps of the TDD Waltz are Fail, Pass and Refactor. But all you see in the screencast is Fail and Pass. The only “refactoring” I do is to use AppCode’s refactoring support to create a new test and alter it. It’s not really refactoring.
Why don’t I show any refactoring? Two reasons:
hasQuerygreatly simplifies the test code. All the hard bits around parsing have already been tested and encapsulated.
This is the beauty of investing in test helpers. Without a mocking framework, I’d have to write my own mock to record the calls to the NSURLSession. This hand-rolled mock would have to give me access to the first argument, the NSURL. And without a matching framework, I’d probably pass the NSURL to a tested helper function. It would all work.
But with the OCMockito + OCHamcrest combination, we get all that functionality in something that’s really easy to write. Better still, it’s also easy to read. (Note that if you prefer OCMock, it also accepts OCHamcrest matchers.)
I have no refactoring to show in this screencast because the TDD is too simple! It’s not always that way, of course. But the more you invest in design skills and unit testing skills, the easier you will find TDD.
Where does it feel like your code resists TDD? Leave a comment below.
Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Design Patterns, Refactoring, and Test-Driven Development (TDD). Programming became fun again! I've now been doing TDD in Apple environments for 17 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy.
Please log in again. The login page will open in a new window. After logging in you can close it and return to this page.