6 TDD Techniques from a Case Study 

 July 21, 2015

by Jon Reid


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 (TDD), the surprising answer is: it doesn’t matter that much, because it’s going to change anyway.

Code snippet sample

Improve your test writing “Flow.”

Sign up to get my test-oriented code snippets.

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.

Move ahead with a failing test, even if you're not certain it's the right one.

Click to Tweet

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]

Case Study: Marvel Comics API Authentication

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:

  • A timestamp
  • A public key
  • An MD5 hash of the timestamp, private key, and public key

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?

Timestamp: Failing Test

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:

- (void)testTimestamp_ShouldChangeEveryCall
    NSString *ts1 = [QCOMarvelAuthentication timestamp];
    NSString *ts2 = [QCOMarvelAuthentication timestamp];
    XCTAssertNotEqualObjects(ts1, ts2);

Should 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.

Timestamp: Passing Code

Now to get it to pass, I did this:

+ (NSString *)timestamp
    return @([[NSDate date] timeIntervalSince1970]).stringValue;

Guided by the idea of a timestamp, this seemed to be the simplest code. The previous test now passes.

Timestamp: Refactoring

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.

Public and Private Keys

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.)

Refactoring: How to Control These Values?

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.

To implement this, I changed timestamp from an instance method to a lazy-loaded, read-only property. (This AppCode template makes it easy to implement lazy-loaded property getters.)

This seems like a good direction to take, so I applied the same changes to the public & private keys: first converting them to instance methods, then to lazy-loaded, read-only properties.

Refactoring Test Duplication

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.

Key Concatenation

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:

- (void)testTimestampedKeys_ShouldConcatenateTimestampPrivateKeyPublicKey
    sut.timestamp = @"Timestamp";
    sut.privateKey = @"Private";
    sut.publicKey = @"Public";
    assertThat(sut.timestampedKeys, is(@"TimestampPrivatePublic"));

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.

The implementation is obvious.

Hashing with MD5

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.

Authorization Parameters: How to Override MD5?

Disclosure: The book links below are affiliate links. If you buy anything, I earn a commission, at no extra cost to you.

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:

@interface TestingMarvelAuthentication : QCOMarvelAuthentication
@implementation TestingMarvelAuthentication
- (NSString *)MD5OfString:(NSString *)str
     return [NSString stringWithFormat:@"MD5%@MD5", str];

This is a testing-specific subclass that overrides the method to make testing easy. Here’s the failing test:

- (void)testURLParameters_ShouldHaveTimestampPublicKeyAndHash
    TestingMarvelAuthentication *sutWithFakeMD5 = [[TestingMarvelAuthentication alloc] init];
    sutWithFakeMD5.timestamp = @"Timestamp";
    sutWithFakeMD5.privateKey = @"Private";
    sutWithFakeMD5.publicKey = @"Public";
    NSString *params = [sutWithFakeMD5 URLParameters];
    assertThat(params, is(@"&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5"));

The implementation is straightforward.

Refactoring Includes Deleting

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.

The best thing to do with a test that no longer adds value is to delete it.

Click to Tweet

Private API, or Just Expose It?

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.

Refactoring Away from Subclass and Override

“Subclass and Override” 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:

- (void)testURLParameters_ShouldHaveTimestampPublicKeyAndHashedConcatenation
    sut.timestamp = @"Timestamp";
    sut.privateKey = @"Private";
    sut.publicKey = @"Public";
    sut.MD5Block = ^(NSString *str) { return [NSString stringWithFormat:@"MD5%@MD5", str]; };
    NSString *params = [sut URLParameters];
    assertThat(params, is(@"&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5"));

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.)

- (NSString *)URLParameters
    return [NSString stringWithFormat:@"&ts=%@&apikey=%@&hash=%@"
            self.timestamp, self.publicKey, self.MD5Block([self timestampedKeys])

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:

- (void)testMD5OfKnownString_ShouldYieldKnownResult // http://tools.ietf.org/html/rfc1321
    NSString *md5 = sut.MD5Block(@"abc");
    assertThat(md5, is(@"900150983cd24fb0d6963f7d28e17f72"));

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.

Techniques in My TDD

What did we learn from this unrehearsed TDD walkthrough? Here’s a summary of the techniques:

  1. Prefer instance methods over class methods. They’re easier to manage for testability. (You can always refactor them to class methods where it makes sense.)
  2. Lazy-loaded properties work well to support property injection with default values.
  3. There are several ways to control an algorithm, including:
    • Subclass and Override Method. (Good for legacy code.)
    • Use the Strategy pattern, defining a protocol. (Good for algorithms expressed as objects.)
    • Inject a block. (Good for simple functions.)
  4. Delete tests that no longer add value! This also means it’s perfectly fine to make tests that you know are stepping stones.
  5. When you notice things to clean up, don’t interrupt the current cycle. Write them down and do them as refactoring steps later.
  6. Don’t be afraid to have design come last. It’s more important to make progress and get feedback quickly, than to spend time trying to figure out the “right” design. You can always refactor your way there. In fact, one way to think about the “red-green-refactor” steps of TDD is “test-code-design.”

One way to think about the "red-green-refactor” steps of TDD is "test-code-design."

Click to Tweet

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 Reid

About the author

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 Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • Great article, Jon! Thanks for taking the time to separate each cognitive change with a tiny commit – it helped me stay focused on what was being changed. That also made viewing the git history beautiful – I could follow along with your mindset, not necessarily the “quickest route to success”.

    I too fall victim to getting locked up before writing a test; it’s hard to pick the right place to start. I can already see that “it doesn’t matter that much, because it’s going to change anyway” is going to help me immensely.

    Number 6 in your summary is also an excellent point. I find my development often swings too far on both sides of that pendulum. (Sometimes I refactor too early while other times I don’t do it enough.) It can be tough to stick to what works: red-green-refactor. But when you do, the benefits largely outweigh any other pattern I’ve used before.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}