0
votes

I've been mocking out accesses to the Parse Data backend and run into trouble with OCMock.

The primary data access mechanism for accessing the backend is through a Parse PFQuery object, constructed with [PFQuery queryWithClassName:@"ClassName"]. This is naturally a good choice as a test seam.

I want to use Partial mocks in this case for reasons I will not go into in this post.

Instead of returning a real PFQuery, I can rig this class method to return a mock object like so:

id queryMock = OCMClassMock([PFQuery class]);
OCMStub([queryMock queryWithClassName:className]).andDo(^(NSInvocation *invocation){
    id mockQuery = OC...;
    NSLog(@">> CREATED MOCK QUERY: %@, %p", mockQuery, &mockQuery);
    [invocation setReturnValue:&mockQuery];
});

... with the mock query's set up like so:

PFQuery *query = [[PFQuery alloc] initWithClassName:className];

id mockQuery = OCMPartialMock(query);

OCMStub([mockQuery findObjectsInBackgroundWithBlock:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
    typedef void (^FindObjectsBlock)(NSArray *, NSError *error);

    FindObjectsBlock callback;
    [invocation getArgument:&callback atIndex:2];

    NSArray *results = evaluateQueryResultArrayFromDataArray(query, objects);

    callback(results, nil);
});

But to mimic the behaviour of the SDK, I would like to be able to call this method multiple times, and I expect distinct mock objects to be produced. This is important.

I chose to use OCMock's class method mocking capabilities. I found that if I invoke the class method twice, the mocking behaviour only applies to the first time. The second time, mocking is ineffective.

        NSLog(@"BEFORE first");
        PFQuery *firstQuery = [PFQuery queryWithClassName:@"THINGS"];
        NSLog(@"AFTER first");

        // Configure first query...

        NSLog(@"BEFORE second");
        PFQuery *secondQuery = [PFQuery queryWithClassName:@"THINGS"];
        NSLog(@"AFTER second");

        // Configure second query...

        expect(secondQuery).toNot.equal(firstQuery);            // OK

        [firstQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
            NSLog(@"THIS IS EXECUTED CORRECTLY %@", objects);   // OK; returns array as expected
        }];

        // The following query FAILS because the real underlying method is called.
        [secondQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
            NSLog(@"THIS DOESN'T EXECUTE RIGHT %@", objects);   // FAILS
        }];

The trace clearly shows the .andDo() behaviour is executed once only.

2014-10-21 16:23:53.186 xctest[18380:209978] BEFORE first
2014-10-21 16:23:53.188 xctest[18380:209978] >> CREATED MOCK QUERY: OCPartialMockObject(PFQuery), 0x7fff5e74d980
2014-10-21 16:23:53.188 xctest[18380:209978] AFTER first
2014-10-21 16:23:53.188 xctest[18380:209978] BEFORE second
2014-10-21 16:23:53.188 xctest[18380:209978] AFTER second

Why is this?

1
Whatever happened to the repetition primitives, like [... times:3]?fatuhoku
Haven't found the time to implement them yet.Erik Doernenburg
(ignore this comment, see answer below)Erik Doernenburg

1 Answers

1
votes

Only one mock object can stub class methods for a given class. The documentation says the effect is undefined when two mock objects try to stub class methods on the same class. In the current implementation a later mock will take over stubbing from the previous mock, meaning that when you create a mock for a given class, all class method stubs created by other mocks for the same class get removed.

In your case you create a mock for PFQuery to stub the queryWithClassName: method. When that stub gets called its implementation creates a new (partial) mock for PFQuery. With the current implementation of OCMock, as described above, that new mock is now the only mock that can stub class methods on PFQuery, but it hasn't declared a stub for the queryWithClassName: method...