If you don’t know what some code should do, you can’t TDD it. A spike solution (a throwaway experiment) gives enough information to begin the TDD process. But now we’re faced with design decisions: What methods do we want? How should they be organized? How should they be accessed?
With Test Driven Development, the surprising answer is: it doesn’t matter that much, because it’s going to change anyway.
This article is longer than usual. Why not start with an executive summary? Listen to iOSBytes episode 80, a 6-minute podcast.
When I began playing electric bass for high school jazz band, I was often uncertain of the right notes to play. Rather than play a wrong note, I opted to stay silent. The band director finally got frustrated and yelled at me: “I don’t care what you play… just play something!” I learned that the bassist’s role is to establish a good rhythm. A wrong note on the beat is better than no note, because it keeps the rhythm moving.
People beginning Test Driven Development sometimes freeze up, not wanting to play a “wrong note.” But the purpose of TDD — to get rapid feedback on your code — won’t happen if you stare and think too long. The way to break “analysis paralysis” is to move ahead with a failing test, even if you’re not certain it’s the right one.
In this post, look over my shoulder as I try to apply this TDD mindset to write code that authorizes requests to the Marvel Comics API. Let’s see if any TDD techniques emerge.[This post is part of the series TDD Sample App: The Complete Collection …So Far]
TDD Sample App: 20 Topics that May Spill Out has led to posts on tooling, project setup, and fast feedback. But we last left the Marvel Browser itself with a spike solution for authorizing calls to the Marvel Comics API. The setup that Marvel has chosen is to pass the following parameters with each request:
Not knowing how to proceed, I made a spike solution to learn how this worked. Now it’s time to apply what we learned from the spike solution. Can we build up the authorization in a stepwise fashion, with tests driving each step?
Marvel’s “Authorizing and Signing Requests” document states that
ts should be “a timestamp (or other long string which can change on a request-by-request basis)”. There’s our first requirement: this value should be different for each request. Here’s the first test I quickly dashed out:
timestamp be a class method or an instance method? Rather than try to analyze our way there, let’s remember that at this point, the most important thing is to move forward. The design will emerge later; for now, just pick something – anything. We can always change our minds. This commit shows my failing test.
Now to get it to pass, I did this:
Guided by the idea of a timestamp, this seemed to be the simplest code. The previous test now passes.
What to refactor? For starters, let’s use OCHamcrest to express the test. In 38a8753 I add OCHamcrest via CocoaPods. (AppCode’s support for CocoaPods makes this really easy.) In 343c152 I rewrite the test assertion using OCHamcrest.
What else? The NSDate documentation tells us that
timeIntervalSinceReferenceDate provides the basis for all the other NSDate methods. So why not use it directly? A quick change verifies that this works. And
timeIntervalSinceReferenceDate is a property these days, so let’s use dot-notation.
Next, we want the public and private keys. I feel conflicted about exposing these as methods, but in the spirit of “move quickly, we can change it later,” let’s just make them visible class methods.
What tests should drive their existence? If this were a shipping app, we could ensure that the keys matched our Marvel developer registration. That would let us to refactor our way to obfuscating the keys in the binary. But since this is an open source example, I want to make it so that you can register with Marvel, get your own keys, and run the app yourself. To make tests that accommodate any keys, I simply test for their expected lengths. One commit is a failing test, another commit returns the appropriate key. (And same for the private key.)
But I’m starting to wonder how other tests will control the values of the timestamp, public key, and private key. It now feels limiting to have these as class methods. Let’s see what happens if we convert
timestamp to an instance method 00b6a526. It seems helpful to have multiple calls to the same instance return the same timestamp, so I wrote a test to drive that behavior 0cbad1c.
There’s duplication in the tests around creation of the SUT (System Under Test). Let’s move the
sut variable into the test fixture — that is, it becomes an instance variable of the test class. (AppCode provides an “Extract Instance Variable” refactoring.) Then let’s create the SUT in the
-setUp method. Observe how the tests slim down.
We want a test to drive something that concatenates the timestamp, public key, and private key. To do so, it’s time to use the ability to have a test control those three values. One way to do this is to take read-only properties and quietly redeclare them read-write. (I actually jumped the gun ahead of TDD and already made them read-write in the .m file. Oh well, let’s continue.) We can expose that read-write access to tests by defining a category on the class under test. This introduces some duplication; let’s make a note to ourselves to resolve that later.
The failing test for concatenation looks like this:
As you can see, this initially drives
timestampedKeys as a property. Later, I change my mind and convert it to a method. Still later, a big change happens to this test. But at the moment, we’re not trying too hard to predict the future. We’re just picking a direction and going for it, to see how it looks and feels.
We need something to calculate the MD5 hash. Should this go into QCOMarvelAuthentication, or into an NSString category? Honestly, we don’t know at this stage, so for the sake of speed, let’s make it an instance method inside the class we already have. Again, we can always change our minds later, because that’s refactoring, which is what all these tests enable.
The failing test checks for the known result of hashing a particular string.
We aren’t going to TDD our way to an MD5 implementation. Instead, I copied a known implementation. But note that the test failed until I got that implementation right! And once it was right, the test enabled me to make small changes, verifying each change.
It’s time to go for the method we wanted in the first place: generating URL parameters for timestamp, public key, and the MD5 hash. We can control each value for testing. But how can we control MD5 hashing to something predictable? There may be a better way, but for now, let’s use Subclass and Override Method, described by Michael Feathers in Working Effectively with Legacy Code. Our MD5 method is named
-MD5OfString:, so here’s how we can control it:
This is a testing-specific subclass that overrides the method to make testing easy. Here’s the failing test:
What to refactor? For starters, I no longer like
timestampedKeys as a property; it feels out of place with the others. Let’s change it to a method.
What about refactoring the tests? I’m looking at the test of
-URLParameters and realizing that it also tests concatenation. So the earlier test for
timestampedKeys is redundant. The best thing to do with a test that no longer adds value is to delete it. So that means we no longer have to expose that method in the header, either. This commit cleans all that up.
Let’s come back to the duplicate declarations of properties as read-write. One way of eliminating that duplication would be to extract it into a separate header file named QCOMarvelAuthentication_Private.h. This could then be imported by the .m file and by any tests that need it.
I’m not completely opposed to this approach, but I am reluctant. As I wrote in Testability, Information Hiding, and the Class Trying to Get Out, I’m afraid this could encourage testing of privates.
Another way would be to change the properties so they’re read-write for everyone. I can see possible uses of that even in shipping code. For example, it could be handy for special debugging modes. So let’s just remove the readonly modifiers and clean up the code.
“Subclass and Override Method” is a great technique for legacy code. But it’s kind of a cheat for fresh code; I think of it as the lowest form of dependency injection. Is there a way we can inject the MD5 calculation? One way would be with another class that satisfies a protocol, declaring “I can make an MD5 hash.” That’s the Strategy design pattern. But for a small function, it seems easier to use a block.
What kind of dependency injection shall we use? Constructor injection is generally preferable, but I don’t want every creator of QCOMarvelAuthentication to have to pass in an identical block. It seems simpler to use property injection, with a default. Let’s duplicate the
-URLParameters test, comment out one copy, and modify the other to drive creation of such a property:
Notice that we’re no longer using the TestingMarvelAuthentication subclass. Let’s make a note to ourselves to clean that up later.
To implement this, we modify
-URLParameters to call the MD5Block property instead of the
-MD5OfString: method. (The old method is still there. Let’s make a note to clean it up later.)
This works. But what if that block property hasn’t been set? We need another test of the default block. Wait, we have already have a test for
-MD5OfString:. Let’s just change that test to call the block instead of the method:
This test drives us to provide a lazy-loaded getter for the block property. Also, I’m discovering that I don’t like the name
MD5Block because it looks funny in the test. A quick, automated renaming to
calculateMD5 looks much better.
Now let’s go back and clean up any unused code. AppCode color-codes unused methods and marks them with a squiggly gray line. Apply “Safe delete” and it’s gone from both the .h and .m files. (Have I mentioned that AppCode is a great tool for TDD?)
Aside from minor formatting changes, we’re done. But how do we know it really works in production? With an acceptance test. I’ll show you in the next post.
What did we learn from this unrehearsed TDD walkthrough? Here’s a summary of the techniques:
What did you learn in this TDD walkthrough, or in your own TDD? What might you do differently? Leave a comment below.[This post is part of the series TDD Sample App: The Complete Collection …So Far]
Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He’s been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.