Adoption Curve Dot Net

Test-driven Development of an Objective-C Category With Kiwi

| Comments

When I first started dabbling with test-driven development, one of the most useful types of resources were tests that other people had written. There’s no shortage of these for Ruby and Rails, partly because it seems like the Rails world was test-driven by default from the outset - but there’s a lot less out there for iOS.

Because of my Rails background, I’m a fan of the Kiwi testing framework - it’s an Rspec-like framework for iOS that brings together a Rspeccy syntax and Objective-C. This is a work through of a set of tests to build a simple category on UIColor. I was scratching my own itch here - designers tend to think in hex values for colours, whereas iOS doesn’t. This method will take a hex string and returns a UIColor object.

Here’s a test-driven process for building said category.

I’m starting with an empty Kiwi project - see here for instructions on how to get to this stage if it’s new to you.  You’ll need to create a new class in the test target - the extended guide cover this.

The source code for the category and the Kiwi tests can be downloaded from https://github.com/timd/UIColor-HexValues.git]

The secret (so I’ve been told, at least) to writing test is to “write the code you wish you had”. In this case we have to cheat slightly, because without the category the project won’t even compile. We need to create it sans tests to begin with:

  • File > New > File > Objective-C category

  • Call the category HexColors and create it on UIColor

  • Add to both targets

Then add the UIColor+HexColors.h file as an import to the test class:

    #import "UIColor+HexColors.h"

TEST ONE

****Now we can start with the tests for real. Writing the code we wish we had, let’s start by wishing that UIColor had a colorWithHexString: method. Here’s the full test class:

 #import "Kiwi.h"
 #import "UIColor+HexColors.h"

 SPEC_BEGIN(ColorTests)

 describe(@"The UIColor class", ^{

    context(@"with a HexColor category", ^{

        it(@"should should respond to the colorFromHex methods", ^{
            [[UIColor should] respondToSelector:@selector(colorWithHexString:)];
        });

    });

 });

 SPEC_END

Ideally, the describe, context and it declarations should read as a sentence, so in this case we’d get:

The UIColor class with a HexColor category should respond to the colorWithHexString: method

This test will fail, because we’ve yet to define that method. Switch to the UIColor+HexColors category, and add the least amount of code that will cause the test to pass.  To start with, that’s declaring the method in the header file:

 +(UIColor *)colorWithHexString:(NSString *)hexString;

In this scenario, we’ve made some assumptions - the method is a class method on UIColor and it will take a single parameter that is an NSString.

Now to implement it:

 +(UIColor *)colorWithHexString:(NSString *)hexString {
    return nil;
 }

Again, an assumption - the compiler demands we return something, so we’re going to return the least that we possibly can - which in this case is nil.

At this point our first test passes. Woo!

TEST TWO

Where to start? This can be tricky - the first test is the hardest to write. I tend to cheat here, and ask myself what would happen if I don’t supply any parameters at all? That question pops up if you take the “I’m going to do as little as possible approach” and apply it to the user - the minimum they can do is call the method with no parameters. If we pass in a nil parameter, we should get nil back - so we’ll test for that:

 it(@"should return nil when given a nil parameter", ^{

 });

Now for the test code:

 it(@"should return nil when given a nil parameter", ^{
   NSString *parameter = nil;
   UIColor *testColor = [UIColor colorWithHexString:parameter];
   [testColor shouldBeNil];
 });

The last line of the method is the actual test itself. Sure enough, this passes - not because we’ve written anything specifically in response to the test, but our minimal method that was enough to get the category to compile is also enough to get our first test passing.

TEST THREE

What’s the next thing we could do? The least complex color I can think of is black - so what should happen if we pass in the hex string for black? The return value should be black as well. So we need to test for RGB values of zero, and an alpha of 1:

 it(@"should return black when given the hex string for black (#000000)", ^{

    NSString *parameter = @"000000";
    UIColor *testColor = [UIColor colorWithHexString:parameter];

    [testColor shouldNotBeNil];

    float rComponent = 0.0f;
    float gComponent = 0.0f;
    float bComponent = 0.0f;
    float aComponent = 0.0f;

    [testColor getRed:&rComponent green:&gComponent blue:&bComponent alpha:&aComponent];

    [[theValue(rComponent) should] equal:theValue(0.0f)];
    [[theValue(gComponent) should] equal:theValue(0.0f)];
    [[theValue(bComponent) should] equal:theValue(0.0f)];
    [[theValue(aComponent) should] equal:theValue(1.0f)];

 });

There’s an extra assertion in there to test that the returned UIColor is not nil - that will prevent this test case continuing if there isn’t a valid object.  Although that passes, the rest of this test fails. Let’s fix it in the simplest possible way - just return black from the method regardless of what parameter is passed in:

 +(UIColor *)colorWithHexString:(NSString *)hexString {
    return [UIColor blackColor];
 }

This fails again, but in an unexpected place. It’s the first test that fails - the one that expects a nil return from a nil parameter. That actually makes sense - we’re returning black for all possible permutations of the parameter. So we need to refactor the method to handle this:

 +(UIColor *)colorWithHexString:(NSString *)hexString {
    if (!hexString) {
        return nil;
    }
    return [UIColor blackColor];
 }

All green, so it’s time for the next test.

TEST FOUR

Having tested black, let’s try the opposite - white:

 it(@"should return white when given the hex string for white (#ffffff)", ^{

    NSString *parameter = @"ffffff";
    UIColor *testColor = [UIColor colorWithHexString:parameter];

    [testColor shouldNotBeNil];

    float rComponent = 0.0f;
    float gComponent = 0.0f;
    float bComponent = 0.0f;
    float aComponent = 0.0f;

    [testColor getRed:&rComponent green:&gComponent blue:&bComponent alpha:&aComponent];

    [[theValue(rComponent) should] equal:theValue(1.0f)];
    [[theValue(gComponent) should] equal:theValue(1.0f)];
    [[theValue(bComponent) should] equal:theValue(1.0f)];
    [[theValue(aComponent) should] equal:theValue(1.0f)];

 });

Unsuprisingly, this fails because we’ve hard-coded the return value. Given that we’re now sending black and white to the UIColor method, the simplest thing I can think of is an if statement to return the appropriate color:

 +(UIColor *)colorWithHexString:(NSString *)hexString {

    if (!hexString) {
        return nil;
    }

    if ([hexString isEqualToString:@"000000"]) {
        return [UIColor blackColor];
    } else {
        return [UIColor whiteColor];
    }

 }

TEST FIVE

There’s a bit of repetition crept into the test, so we can refactor that out. The xComponent floats can be relocated to the top of the class as __block parameters so they can be passed in and modified in the test block:

 __block float rComponent = 0.0f;
 __block float gComponent = 0.0f;
 __block float bComponent = 0.0f;
 __block float aComponent = 0.0f;

Black and white works, so it’s time for another color. Let’s try red:

 it(@"should return red when given the hex string for red (#ff0000)", ^{

    NSString *parameter = @"ff0000";
    UIColor *testColor = [UIColor colorWithHexString:parameter];

    [testColor shouldNotBeNil];

    [testColor getRed:&rComponent green:&gComponent blue:&bComponent alpha:&aComponent];

    [[theValue(rComponent) should] equal:theValue(1.0f)];
    [[theValue(gComponent) should] equal:theValue(0.0f)];
    [[theValue(bComponent) should] equal:theValue(0.0f)];
    [[theValue(aComponent) should] equal:theValue(1.0f)];

 });

This fails, pretty much as we suspect it might. But this is an interesting inflection point - having added a third test case, this is the point where we have to build our method for real. There isn’t really another other smaller step that we can implement. So here goes with the big change:

 +(UIColor *)colorWithHexString:(NSString *)hexString {
    if (!hexString) {
    return nil;
 }

    NSRange rRange = NSMakeRange(0, 2);
    NSString *rComponent = [hexString substringWithRange:rRange];
    NSUInteger rVal = 0;
    NSScanner *rScanner = [NSScanner scannerWithString:rComponent];
    [rScanner scanHexInt:&rVal];
    float rRetVal = (float)rVal / 254;

    NSRange gRange = NSMakeRange(2, 2);
    NSString *gComponent = [hexString substringWithRange:gRange];
    NSUInteger gVal = 0;
    NSScanner *gScanner = [NSScanner scannerWithString:gComponent];
    [gScanner scanHexInt:&gVal];
    float gRetVal = (float)gVal / 254;

    NSRange bRange = NSMakeRange(4, 2);
    NSString *bComponent = [hexString substringWithRange:bRange];
    NSUInteger bVal = 0;
    NSScanner *bScanner = [NSScanner scannerWithString:bComponent];
    [bScanner scanHexInt:&bVal];
    float bRetVal = (float)bVal / 254;

    return [UIColor colorWithRed:rRetVal green:gRetVal blue:bRetVal alpha:1.0f];

 }

This is less complicated than it looks. Firstly, it splits the hexString into the R, G and B components by grabbing ranges of two characters at a time:

 NSRange rRange = NSMakeRange(0, 2);
 NSString *rComponent = [hexString substringWithRange:rRange];

Next the two-character string is convert to the integer equivalent:

 NSUInteger rVal = 0;
 NSScanner *rScanner = [NSScanner scannerWithString:rComponent];
 [rScanner scanHexInt:&rVal];

The decimal value is divided by 254 to get a float value in the range 0 to 1.0:

 float rRetVal = (float)rVal / 254;

Finally the UIColor is built using the float values (and an alpha of 1.0):

 return [UIColor colorWithRed:rRetVal green:gRetVal blue:bRetVal alpha:1.0f];

And at this point, we’ve got four passing tests.

TEST SIX

That should be enough to cover all the permutations of hex values that could be passed in, but let’s add another test just to be sure - this time 7f :

 it(@"should return muddy green when given the hex string for muddy green (#007f00)", ^{

    NSString *parameter = @"007f00";
    UIColor *testColor = [UIColor colorWithHexString:parameter];

    [testColor shouldNotBeNil];

    [testColor getRed:&rComponent green:&gComponent blue:&bComponent alpha:&aComponent];

    [[theValue(rComponent) should] equal:theValue(0.0f)];
    [[theValue(gComponent) should] equal:theValue(0.5f)];
    [[theValue(bComponent) should] equal:theValue(0.0f)];
    [[theValue(aComponent) should] equal:theValue(1.0f)];

 });

That passes, so we can be reasonably confident that our method handles valid hex colors.

TEST SEVEN

But what about invalid values? Let’s try testing for a situation where the method gets called with a parameter that’s too short:

 it(@"should return nil if given too few parameters", ^{
    NSString *parameter = @"abcd";
    UIColor *testColor = [UIColor colorWithHexString:parameter];
    [testColor shouldBeNil];
 });

It fails, so it needs fixing:

 if ([hexString length] < 6) {
    return nil;
 }

TEST EIGHT

We could also pass in too many characters:

 it(@"should return nil if given too many parameters", ^{
    NSString *parameter = @"abcdefgh";
    UIColor *testColor = [UIColor colorWithHexString:parameter];
    [testColor shouldBeNil];
 });

Another fail, another fix:

 if ([hexString length] < 6) {
    return nil;
 }

 if ([hexString length] > 6) {
    return nil;
 }

But hold on, that could be refactored to get rid of two if clauses - the check on the parameter’s length and the check for a nil parameter:

 if ([hexString length] != 6) {
    return nil;
 }

Repeat the tests, and everything is still green. That’s one of the main benefits of testing - you can refactor with confidence, knowing that you haven’t broken anything with the changes.

TEST NINE

What about a scenario where six characters are passed in, but it’s not a hex value? Let’s test that:

 it(@"should return nil if given non-hex numeric characters", ^{
    NSString *parameter = @"abc&qr";
    UIColor *testColor = [UIColor colorWithHexString:parameter];
    [testColor shouldBeNil];
 });

Fixing this needs a little more thought. What we need to let through is 0-9 and a-f (and also A-F just to be sure). Anything else should be rejected.

This sounds like a job for a regex:

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-fA-F|0-9]" options:0 error:NULL];
    NSUInteger noOfMatches = [regex numberOfMatchesInString:hexString options:NSMatchingCompleted range:NSMakeRange(0, [hexString length])];

    if (noOfMatches != 0) {
        return nil;
    }

Working through this, we first create a regex to match anything that ISN’T a to f or A to F or 0 to 9:

 [^a-fA-F|0-9]

Then we search the parameter to see if there’s any matches - if there are, it’s as a result of a rogue character and the method should return nil.

Run this, and once again we’re all green.

ALL DONE!

This might seem like an awful lot of work for a trivial method, and to a certain extent it is. But by working through the problem this way, we’ve gained several benefits:

  • the problem’s been broken down into a number of smaller discrete steps

  • it’s encouraged us to look at how to handle edge cases and error situations

  • the tests mean that it’s less likely that we’ll break something in the process of trying to fix something else

  • the tests have documented the intent of the code in a way that’s expressed in (nearly) plain English

While this might not be too big a deal with a simple method like this, testing comes into its own when you’re building code that will talk to an API or parse file input, for example - by having a test suite that can run against your code, you can play around with changing things knowing that you’re probably not going to break anything else as you do. And for me, the step-by-step iterative approach means I’m forced to approach the problem in a logical way, building the solution up bit-by-bit.

Comments