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.
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.
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.
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.
✦ PermalinkMy books...