Test-driven development (TDD) doesn't make anything happen automatically. You really need to level up on two other skills as you go: design, and unit testing. Doing so can shift TDD from being daunting to being simple.
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:
- names starting with NAME
- paging the results into chunks of 10
- skipping the first 3 pages, so starting from 30th result
we want to generate a URL with a query string. Something like:
http://gateway.marvel.com/v1/public/characters?nameStartsWith=NAME&limit=10&offset=30
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,
- A single test should examine one argument at a time, not all three.
- The order of arguments can change and still be correct. So an equality check would prohibit valid results.
- The query string will soon have more arguments. Specifically, we need to add authentication.
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.
Outline:
- TDD the URL path (0:00)
- TDD the nameStartsWith parameter (2:54)
- TDD percent-encoding of the parameter (5:17)
Show notes:
- TDD Networking with DI: Is It Really That Easy?
- Marvel API documentation
- How OCHamcrest’s “hasProperty” Can Make Your Tests Simpler
- AppCode vs. Xcode Unit Testing Battle
- Can OCMockito’s New Error Reporting Save You Time?
- How to Make Your Own OCHamcrest Matcher for Powerful Asserts
AppCode’s “Refactor This” Menu
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.
TDD Cheating
I did cheat in my TDD. At 4:22, I had 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
. I probably should have. But in this video, I took the cheating 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.
Where’s the Refactoring?
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:
- This is only the first query parameter. There are two more in the request model, plus the ones that come from authentication. We simply don’t have enough changes yet.
- The custom
hasQuery
matcher greatly 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.
[This post is part of the series TDD Sample App: The Complete Collection …So Far]
The more you invest in design skills and unit testing skills, the easier you will find TDD.