Skip to content

TDD and crypto in one place

Well, I suppose if I’ve written two books, it’s about time I wrote a contorted blog post that references both of the worlds.

I recently wrote an encryption module for an app, and thought it’d be useful to share something about the design process. Notice that the source code here was quickly thrown together for the purposes of demonstrating the design technique, and bears little resemblance to the code I was actually writing (which was in a different language). A lot of the complexity I was actually trying to deal with has been elided in order to bring the story to the fore.

Steps 1a, 2a, 1b, 2b etc: Design the API hand-in-hand with a fake implementation.

A pass-thru implementation would work here, but I prefer to use something that actually does change its input just so that it’s clear that there’s a requirement the input should be acted on. You might end up with tests that look something like this:

@implementation CryptoDesignTests
{
    id <StringEncryptor>cryptor;
}

- (void)setUp {
    cryptor = [[ROT13StringEncryptor alloc] init];
}

- (void)tearDown {
    cryptor = nil;
}

- (void)testEncryptionOfPlainText {
    NSString *plainText = @"hello";
    NSString *cipherText = [cryptor encipherString: plainText];
    STAssertEqualObjects(@"uryyb", cipherText, @"This method should encrypt its parameter");
}

- (void)testDecryptionOfCipherText {
    NSString *cipherText = @"uryyb";
    NSString *plainText = [cryptor decipherString: cipherText];
    STAssertEqualObjects(@"hello", plainText, @"This method should decrypt its parameter");
}

@end

Where the protocol looks like this:

@protocol StringEncryptor <NSObject>

- (NSString *)encipherString: (NSString *)plainText;
- (NSString *)decipherString: (NSString *)cipherText;

@end

Of course you can implement this however you want. Here’s one potential implementation (which may have its problems, but is only being used to guide the API design):

@interface ROT13StringEncryptor : NSObject <StringEncryptor>

@end

@implementation ROT13StringEncryptor

- (NSString *)rot13: (NSString *)original {
    NSMutableData *originalBytes = [[original dataUsingEncoding: NSASCIIStringEncoding] mutableCopy];
    for (NSInteger i = 0; i < [originalBytes length]; i++) {
        NSRange cursor = NSMakeRange(i, 1);
        char c;
        [originalBytes getBytes: &c range: cursor];
        if       (c >= 'a' && c <= 'm') c += 13;
        else if  (c >= 'n' && c <= 'z') c -= 13;
        else if  (c >= 'A' && c <= 'M') c += 13;
        else if  (c >= 'A' && c <= 'Z') c -= 13;
        [originalBytes replaceBytesInRange: cursor withBytes: &c];
    }
    return [NSString stringWithCString: [originalBytes bytes] encoding: NSASCIIStringEncoding];
}

- (NSString *)encipherString:(NSString *)plainText {
    return [self rot13: plainText];
}

- (NSString *)decipherString:(NSString *)cipherText {
    return [self rot13: cipherText];
}

@end

Step 3. Write an integration test.

Having designed the API for the class, I'd like to write an integration test that does whatever it is that the app is trying to achieve. In my real app this task was that two cryptors hooked up to each other should agree on a key and pass a message between each other. For this demo, we'll require that the cryptor can round-trip a message and end up with the original text.

@implementation CryptoRoundTripTests
{
    id <StringEncryptor>> cryptor;
}

- (void)setUp {
    cryptor = [[ROT13StringEncryptor alloc] init];
}

- (void)tearDown {
    cryptor = nil;
}

- (void)testRoundTripEncryption {
    NSString *plain = @"Mary had a little lamb.";
    NSString *outOfTheSausageMachine = [cryptor decipherString: [cryptor encipherString: plain]];
    STAssertEqualObjects(plain, outOfTheSausageMachine, @"Content should survive an encryption round-trip");
}

@end

This test passes without any extra work.

Step 4. Give that lot to the poor sap who needs to write the app.

That's right, the other developers can start working straight away. They've got the protocol which tells them what the object can do, the unit tests which explain how the API should work and the integration test that explains how you expect the object to be used.

Leaving you free to:

Step 5. Point the integration test at a different implementation of the protocol.

But you haven't written that implementation yet! It must be time to:

Step 6. Write it.

You know when it works because you have integration tests. You know it'll work with the app the other person's writing because they've got the same integration test.

So the extra artefacts that some people see as a wasteful byproduct of code-level testing — the fake implementation of the protocol for example — are demonstrably useful in this scenario. Other developers on the team can use the fake implementation as a stand-in while you're head-down coding up the production code. It's also much easier to design a class when you're not also sweating over how the internal details are going to work out, so you probably get where you're going quicker too.