Monday April 04, 2011

✦ Kiwi Library

Kiwi is an Rspec like clone for Objective-C written by Allen Ding. Kiwi uses the new block syntax in Snow Leopard and iOS 4.x to define groups of assertions and share setup state between collections of tests. If you’re familiar with Rspec in Ruby or Jasmine in Javascript you’ll recognize a lot. It comes with mocks, stubs and “matchers” that let you make descriptive assertions. It’s built on top of SenTestingKit that Apple already endorses and bundles with Xcode.

To show it off, lets say we want to test the aggregate functions in key value coding (KVC).

#import <Foundation/Foundation.h>
#import "Kiwi.h"

SPEC_BEGIN(NSArrayKVCAggregateFunctionSpec)

describe(@"sum", ^{
    // Block scope variable is shared with *everything* in here
    __block NSArray *collection = nil;

    it(@"adds up the key path", ^{
        // Leave the collection empty so we can see a failure
        NSNumber *sum = [collection valueForKeyPath:@"@sum.age"];
        [[sum should] equal:[NSNumber numberWithInt:20]];
    });
});

SPEC_END

Note the SPEC_BEGIN and SPEC_END macros. The preprocessor uses them to build the interface and implementation of a normal SenTestCase subclass. SPEC_BEGIN’s sole argument is the actual sublcass name. It needs to be a valid class identifier and a unique symbol in the application.

The strings passed to describe and it macros become part of the test output and error reporting when the tests fail. The should method returns an object that responds to matcher methods like the equal: above. (The should method is added to NSObject so anything can start triggering an assertion.)

Run the test and you see this:

error: -[NSArrayKVCAggregateFunctionSpec runSpec] :
    "sum adds up the key path" FAILED, expected subject not to be nil

Xcode drops you right on the line with the “should…equal” assertion. Note how the NSString from describe and it blocks are concatenated together. And the test failed because it knows asserting NSObject equality with nil isn’t very useful. (There’s a beNil matcher specifically for that.)

Now that we know our tests are running and can fail, let’s set the state up properly to make it pass.

// Setting up the array to satisfy the condition
describe(@"sum", ^{
    __block NSArray *collection = nil;
    __block NSDictionary *person1 = nil, *person2 = nil;

    beforeEach(^{
        person1 = [NSDictionary dictionaryWithObject:
            [NSNumber numberWithInt:15] forKey:@"age"];
        person2 = [NSDictionary dictionaryWithObject:
            [NSNumber numberWithInt:5] forKey:@"age"];
        collection = [NSArray arrayWithObjects:person1, person2, nil];
    });

    it(@"adds up the key path", ^{
        NSNumber *sum = [collection valueForKeyPath:@"@sum.age"];
        [[sum should] equal:[NSNumber numberWithInt:20]];
    });
});

The beforeEach method is analogous to the setUp in SenTestCase subclasses. You win the prize if you guessed afterEach is like tearDown. In our setup, we add two objects with the key “age” to the collection. Running the tests should now pass with this output:

Test Suite 'NSArrayKVCAggregateFunctionSpec' started at 2011-04-02 18:28:28 +0000
Test Case '-[NSArrayKVCAggregateFunctionSpec runSpec]' started.
Test Case '-[NSArrayKVCAggregateFunctionSpec runSpec]' passed (0.039 seconds).
Test Suite 'NSArrayKVCAggregateFunctionSpec' finished at 2011-02-04 18:28:28 +0000.
Executed 1 test, with 0 failures (0 unexpected) in 0.039 (0.040) seconds

Boom.

Note that you can nest describe blocks within other describe blocks. Using block scope you can share test state with the nested describe’s to help cut down on ceremonial code noise. Read the warning on this below, though.

What’s to like?

I already use this framework in several projects. I come from a Ruby background and fell in love with the nested context structure that Rspec gave us. When used wisely, you can construct great test code without all the ceremony of setting up the subclass structure of SenTestCase. To be honest, I’d rather have MacRuby running on the iPhone (fingers crossed). Then I can do these tests with the even more elegant syntax of Ruby’s blocks. In the meantime, this does the work just fine.

Caveat Develepor

It can be overwhelming to Rspec/Jasmine newcomers. Nesting blocks gets unwiedly if you’re not careful. You can find yourself five or more levels deep while trying really hard to reuse test state and end up with unreadable tests. That’s a common n00b problem with this syntax. I’d recommend checking out the Growing A Test Suite screencast on Destroy All Software for a great introduction to keeping your tests trim.

It’s a large framework bolted on top of SenTestingKit. If SenTestingKit was deprecated or changed significantly, your Kiwi specs may break. Given Apple’s promotion and the inertia behind SenTestingKit I doubt this is a problem. Xcode 4 has the best support for SenTestingKit I’ve seen yet. Their default test templates have everthing set up for debugging right in the tests, if you need it. So far, I’m convinced they’re in it for the long haul.

It depends a lot on the magic behind the Objective-C runtime. It bolts should and shouldNot onto NSObject. It does a lot of fancy selector forwarding to get the matchers to work. None of this is “unorthodox” by Apple’s standards, though. While browsing through the code, I didn’t see anything I myself couldn’t tweak if some of the ObjC runtime API’s were changed. Again, the inertia here is so strong, that I doubt this is a problem.

So?

If you’re up for experimenting, I’d recommend it. For seasoned ObjC devs familiar with Rspec, I’d even recommend it for production use. Make sure you dig in and get to know it. Read through the headers. You’ll see lots of matchers you can use. There’s fascinating stuff in this library.