Here\'s the method under test:
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass {
NSDictionary *userPassD = @{@\"user\":userName,
Code I'm working with is heavily based on blocks, so I'm super familiar with your question.
Just to rephrase problem a bit:
- (ReturnObject *)methodThatWeWantToTest:(Foobar *)bar
{
[self.somethingElse doSomethingAndCallBlock:^{
// really want to test this code
} secondBock:^{
// even more code to test
}];
}
To solve exactly the same problem you're bringing up here, we've created unit test helper class that has methods with identical signature to methods that call blocks that we need to test.
For you code sample, it is a mock method that returns nothing, takes one id argument and two blocks.
Below is example of the mock method you'd need and sample of the OCMock unit test that utilizes
- (void)mockSuccessBlockWithOneArgumentTwoBlocks:(id)firstArgument
successBlock:(void (^)(NSString *authtoken))successBlock
failureBlock:(void (^)(NSString *errorMessage))failureBlock
{
successBlock(@"mocked unit test auth token");
}
- (void)testLoginWithUserCallsSomethingInCredStoreOnLoginSuccess
{
LoginService *loginService = [[LoginService alloc] init];
// init mocks we need for this test
id credStoreMock = [OCMockObject niceMockForClass:[MyCredStore class]];
id loginCtrlMock = [OCMockObject niceMockForClass:[MyLoginCtrl class]];
// force login controller to call success block when called with loginWithUserPass
// onObject:self - if mock method is in the same unit test, use self. if it is helper object, point to the helper object.
[[[loginCtrlMock stub] andCall:@selector(mockSuccessBlockWithOneArgumentTwoBlocks:successBlock:failureBlock::) onObject:self] loginWithUserPass:OCMOCK_ANY withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];
// setup mocks
loginService.credStore = credStoreMock;
loginService.loginCtrl = loginCtrlMock;
// expect/run/verify
[[credStore expect] callSomethingFromSuccessBlock];
[loginService loginWithUser:@"testUser" andPass:@"testPass"];
[credStore verify];
}
Hope it helps! Let me know if you have any questions, we've got it working.
I think you can do this with a spy. Reading this wiki page on spies it looks like you can capture the block passed in and invoke it yourself in the test.
If I follow you right, this may do what you want:
@interface ExampleLC : NSObject
- (void)loginWithUserPass:userPassD withSuccess:(void (^)(NSString *authToken))successBlock failure:(void (^)(NSString *errorMessage))failureBlock;
@end
@implementation ExampleLC
- (void)loginWithUserPass:userPassD withSuccess:(void (^)(NSString *authToken))successBlock failure:(void (^)(NSString *errorMessage))failureBlock
{
}
@end
@interface Example : NSObject {
@public
ExampleLC *_loginCntrl;
}
- (void)saveToken:(NSString *)authToken;
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass;
@end
@implementation Example
- (void)saveToken:(NSString *)authToken
{
}
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass {
NSDictionary *userPassD = @{@"user":userName,
@"pass":pass};
[_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){
// save authToken to credential store
[self saveToken:authToken];
} failure:^(NSString *errorMessage) {
// alert user pass was wrong
}];
}
@end
@interface loginTest : SenTestCase
@end
@implementation loginTest
- (void)testExample
{
Example *exampleOrig = [[Example alloc] init];
id loginCntrl = [OCMockObject mockForClass:[ExampleLC class]];
[[[loginCntrl expect] andDo:^(NSInvocation *invocation) {
void (^successBlock)(NSString *authToken) = [invocation getArgumentAtIndexAsObject:3];
successBlock(@"Dummy");
}] loginWithUserPass:OCMOCK_ANY withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];
exampleOrig->_loginCntrl = loginCntrl;
id example = [OCMockObject partialMockForObject:exampleOrig];
[[example expect] saveToken:@"Dummy"];
[example loginWithUser:@"ABC" andPass:@"DEF"];
[loginCntrl verify];
[example verify];
}
@end
This code allows forces the real success block to be invoked with the argument you specify, which you can then verify.