最近書いているアプリでは、ISHTTPOperationという通信用のNSOperationを採用しています。
使い方はNSURLConnectionsendAsynchronousRequest:queue:completionHandler:と大体同じです。

NSURL *URL = [NSURL URLWithString:@"http://date.jsontest.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
[ISHTTPOperation sendRequest:request handler:^(NSHTTPURLResponse *response, id object, NSError *error) {
    if (error) {
        return;
    }
    // completion
}];

で、通信を含む処理のテストを書くとき、完了ハンドラの引数に任意の値を入れたかったりします。
なので、ISHTTPOperationをモック化するISHTTPOperationMockifierというのをつくりました。

ISHTTPOperation
ISHTTPOperationMockifier

使い方

  • ISHTTPOperationMockifierをつくる
  • 完了ハンドラに入れたい値をmockifierにセットする
  • [mockifier mockify]
- (void)testSuccessCase
{
    NSDictionary *dictionary =  @{@"hogeKey": @"hogeValue"};

    ISHTTPOperationMockifier *mockifier = [[ISHTTPOperationMockifier alloc] init];
    mockifier.statusCode = 200;
    mockifier.object = dictionary;
    mockifier.error = nil;
    [mockifier mockify];

    [ISHTTPOperation sendRequest:_request handler:^(NSHTTPURLResponse *response, id object, NSError *error) {
        STAssertEqualObjects([object objectForKey:@"hogeKey"], [dictionary objectForKey:@"hogeKey"], nil);
        [self endWaiting];
    }];

    [self beginWaiting];
}

仕組み

mockifyの中身は以下のようになっていて、 ```objectivec - (void)mockify { if (self.isMockified) { return; }

Class class = [ISHTTPOperation class];
objc_setAssociatedObject(class, ISHTTPOperationMockStatusCodeKey, @(self.statusCode), OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(class, ISHTTPOperationMockObjectKey, self.object, OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(class, ISHTTPOperationMockErrorKey, self.error, OBJC_ASSOCIATION_RETAIN);

ISSwizzleInstanceMethod(class, @selector(main), @selector(_main));
self.mockified = YES; } ```

ISHTTPOperationmainが以下のものに差し替えられます。

- (void)_main
{
    dispatch_async(dispatch_get_main_queue(), ^{
        Class class = [ISHTTPOperation class];
        NSInteger statusCode = [objc_getAssociatedObject(class, ISHTTPOperationMockStatusCodeKey) integerValue];
        id object = objc_getAssociatedObject(class, ISHTTPOperationMockObjectKey);
        NSError *error = objc_getAssociatedObject(class, ISHTTPOperationMockErrorKey);
        
        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL
                                                                  statusCode:statusCode
                                                                 HTTPVersion:@"1.1"
                                                                headerFields:nil];
        
        self.handler(response, object, error);
    });
    
    [self completeOperation];
}

通信をする部分を、予めセットした値を引数とするハンドラの実行にすり替えたわけです。
このすり替えは、ISHTTPOperationMockifierが生存している間のみ有効です。

実際の例

ViewControllerに以下のような処理があったとします。

- (void)refresh
{
    NSURL *URL = [NSURL URLWithString:@"http://date.jsontest.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    [ISHTTPOperation sendRequest:request handler:^(NSHTTPURLResponse *response, id object, NSError *error) {
        if (error || response.statusCode != 200) {
            return;
        }
        
        [self insertObjectsWithData:object];
    }];
}

- (void)insertObjectsWithData:(NSData *)data
{
    // NSManagedObjectをつくったりする
}

これに対して以下のようなテストを書くことができます。

- (void)testUpdateData
{
    NSData *data = [@"hoge" dataUsingEncoding:NSUTF8StringEncoding];

    ISHTTPOperationMockifier *mockifier = [[ISHTTPOperationMockifier alloc] init];
    mockifier.statusCode = 200;
    mockifier.object = data;
    mockifier.error = nil;
    [mockifier mockify];
    
    id mock = [OCMockObject partialmockiforObject:_viewController];
    [[mock expect] insertObjectsWithData:[OCMArg any]];
    
    [_viewController refresh];
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
    
    STAssertNoThrow([mock verify], nil);
}

- (void)testFailUpdatingData
{
    NSError *mockError = [NSError errorWithDomain:@"ISHTTPOperationDomain"
                                             code:-1234
                                         userInfo:nil];
    
    ISHTTPOperationMockifier *mockifier = [[ISHTTPOperationMockifier alloc] init];
    mockifier.statusCode = 0;
    mockifier.object = nil;
    mockifier.error = mockError;
    [mockifier mockify];
    
    id mock = [OCMockObject partialmockiforObject:_viewController];
    [[mock expect] insertObjectsWithData:[OCMArg any]];
    
    [_viewController refresh];
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
    
    STAssertThrows([mock verify], nil);
}

1個目は通信成功時にinsertObjectsWithData:が呼ばれるテストで、
2個めは通信失敗時にinsertObjectsWithData:が呼ばれないテストです。