I’ve recently started looking at other unit testing frameworks. I’ve been using OCUnit, GHUnit, and OCMock. But I’m finding that as my tests become more extensive and numerous, the tests themselves are becoming more difficult to read. I have been able to test everything that I’ve tried using these tools, but the resulting test code is sometimes hard to read. So my objective in looking at other tools is not for additional functionality, but for better readability.
The first tools that I looked at were OCHamcrest and OCMockito by Jon Reid. I like the syntax provided by these. But I ran into a problem when converting some of my tests because they currently use partial mocks. It appears that the Java version Mockito provides something like partial mocks, using what is called “spy”, but that this capability hasn’t been ported to OCMockito yet.
So while contemplating whether to try pushing forward using categories, swizzling, or subclassing to replace partial mocks, another iOS engineer recommended that I give Kiwi a look. So I did, and it looks very promising. I guess I haven’t given Kiwi a good look before because I heard that it was only for BDD, not unit testing. This turns out not to be the case.
I am going to give Kiwi a workout by converting the tests in one of the existing unit test files in What’s My Speed: the WeatherService. This file contains an assortment of OCUnit tests using both mock and partial mock OCMock objects.
Adding Kiwi to the project and file
The first step is to add Kiwi to the project. I’m going to just add the static library and headers, but there are instructions for adding it as a subproject. I built the code from source following the instructions on the Kiwi website. I then added the static lib and headers to the project, added the Kiwi headers directory to the test target headers search path, and then the kiwi.h to the WeatherServiceTests.m file:
#import "Kiwi.h"
I’m going to try leaving OCMock in the file until all of the tests have been completed. Then rebuild to verify everything ok.
Note: I had to clean and build twice before it would build without errors. This is a long time Xcode bug. Sometimes Xcode appears to cache things. Upon making changes to header paths and such, it sometimes takes a couple builds before strange errors go away. In this case, it was reporting that it couldn’t find WeatherService.h. Build cleaning and rebuilding twice and the reported error went away.
I also encountered an error with missing header files, including NSObject+KiwiSpyAdditions.h. It appears that building Kiwi inside the Xcode IDE results in only part of the header files being copied to the build directory. I fixed this by manually copying the headers from the Kiwi/Kiwi source directory to my project’s Kiwi headers directory.
Converting the first few simple tests
Next I’ll convert the first few tests. These are simple tests that verify the basic singleton operation of the object. So here is the existing tests before converting. I’ve removed some lines that aren’t related to these tests, and I’ll add them back as we go.
#import <SenTestingKit/SenTestingKit.h>
#import <OCMock/OCMock.h>
#import "WeatherService.h"
#import "WeatherService-Private.h"
@interface WeatherServiceTests : SenTestCase
@property (nonatomic, strong) WeatherServiceForTesting *weatherService;
@end
@implementation WeatherServiceTests
- (void)setUp {
self.weatherService = [[WeatherServiceForTesting alloc]init];
}
- (void)tearDown {
self.weatherService = nil;
}
- (void)testInstantiation {
STAssertNotNil(self.weatherService, @"Test instance is nil");
}
- (void)testSharedInstanceNotNil {
WeatherService *ws = [WeatherService sharedInstance];
STAssertNotNil(ws, @"sharedInstance is nil");
}
- (void)testSharedInstanceReturnsSameSingletonObject {
WeatherService *ws1 = [WeatherService sharedInstance];
WeatherService *ws2 = [WeatherService sharedInstance];
STAssertEquals(ws1, ws2, @"sharedInstance didn't return same object twice");
}
Ok, pretty straight forward tests, no mocks needed. Let’s convert these to Kiwi:
#import <SenTestingKit/SenTestingKit.h>
#import <OCMock/OCMock.h>
#import "WeatherService.h"
#import "WeatherService-Private.h"
#import "Kiwi.h"
SPEC_BEGIN(WeatherServiceKiwiTests)
describe(@"Singleton (by choice)", ^{
it(@"should instantiate using init", ^ {
[[[WeatherService alloc]init] shouldNotBeNil];
});
it(@"should instantiate using sharedInstance", ^{
[[WeatherService sharedInstance] shouldNotBeNil];
});
it(@"should return the same instance twice using sharedInstance", ^{
WeatherService *a = [WeatherService sharedInstance];
WeatherService *b = [WeatherService sharedInstance];
[[a should] beIdenticalTo:b];
});
it(@"should not return the same instance twice using init", ^{
WeatherService *a = [[WeatherService alloc] init];
WeatherService *b = [[WeatherService alloc] init];
[[a shouldNot] beIdenticalTo:b];
});
});
SPEC_END
Now let’s test to make sure the tests are actually working. Cmd+U to execute tests, and everything appears ok. Are the tests actually working? To verify this, I reverse the test logic by replacing “should” with “shouldNot”, and “ShouldNotBeNil” with “shouldBeNil”, rerunning the tests I see the failures. So I have some confidence that the tests are doing what I expect them to be doing.
Our next test methods further verify that init is doing what we expect. It calls 2 other methods, that each do just one thing.
- (WeatherService *)init {
self = [super init];
if (self) {
[self startTimer];
[self updateWeatherInfoForZipcode:kDEFAULT_ZIPCODE];
}
return self;
}
Ok, so with OCMock we used a partial mock in two tests:
- (void)testInitCallsStartTimer {
id mock = [OCMockObject partialMockForObject:self.weatherService];
[[mock expect]startTimer];
id __unused initedMock = [mock init];
[mock verify];
}
- (void)testInitCallsUpdateWeatherInfoForZipcode {
id mock = [OCMockObject partialMockForObject:self.weatherService];
[[mock expect]updateWeatherInfoForZipcode:kDEFAULT_ZIPCODE];
id __unused initedMock = [mock init];
[mock verify];
}
Since Kiwi appears to have great support mocks, this should be pretty straight forward. Note that Kiwi’s mock support allows defining stubs and expectations on both mocks and objects. This eliminates the need for partial mocks altogether!.
describe(@"init", ^{
it(@"starts the timer", ^ {
id weatherService = [[WeatherService alloc]init];
[[weatherService should] receive:@selector(startTimer)];
id __unused initedMock = [weatherService init];
});
it(@"updates the weather info", ^{
id weatherService = [[WeatherService alloc]init];
[[weatherService should] receive:@selector(updateWeatherInfoForZipcode:) withArguments:kDEFAULT_ZIPCODE];
id __unused initedMock = [weatherService init];
});
});
Ok, this new code looks pretty similar. It’s shorter by one line because [mock verify] isn’t needed. And for this small set of fairly simple tests, the difference in readability isn’t much, but I’m seeing the potential for greatly improved readability. The structure of the tests feels much more organized. I need to learn how to really take advantage of that. I’m going to stop this blog here, and continue converting the rest of the tests to Kiwi. I’ll probably have more to say about this in future posts as I learn more about using Kiwi.