2

I would like to test that a method is called twice, the first time passing YES for a parameter, the second time NO. What complicates things is that the method I would like to test is a class method, I'm not sure whether this has anything to do with the issue I'm seeing.

My test looks like this:

- (void)testCreatesMessagesWithCorrectHTMLForcing {
    id messageClassMock = OCMClassMock([MyMessage class]);
    [messageClassMock setExpectationOrderMatters:YES];
    [[[messageClassMock expect] andForwardToRealObject] messageForDictionary:[OCMArg any]
                                                          forceHTMLRendering:YES
                                                                   inContext:[OCMArg any]];
    [[[messageClassMock expect] andForwardToRealObject] messageForDictionary:[OCMArg any]
                                                          forceHTMLRendering:NO
                                                                   inContext:[OCMArg any]];
    NSMutableDictionary *mockJSON = [self.mockJSON mutableCopy];
    MyThread *classUnderTest = [MyThread threadForDictionary:mockJSON
                                                                       inContext:self.mockContext];
    OCMVerifyAll(messageClassMock);
    [messageClassMock stopMocking];
}

The threadForDictionary:inContext: method calls the messageForDictionary:forceHTMLRendering:inContext: for each message in the thread and needs an object as return value. That's why I added andForwardToRealObject, otherwise I get exceptions because of the return value being nil. As you can imagine from the signatures it's about parsing JSON to CoreData objects.

Adding this test makes all other tests in the same test file fail with the following message

unexpected method invoked: messageForDictionary:<OCMAnyConstraint: 0x7fab637063d0> forceHTMLRendering:NO inContext:<OCMAnyConstraint: 0x7fab63706eb0>
expected:   messageForDictionary:<OCMAnyConstraint: 0x7fab6371cb20> forceHTMLRendering:YES inContext:<OCMAnyConstraint: 0x7fab6371fb50>"

I don't get why this happens as I call stopMocking in the end so the other tests should not be affected.

The following changes make the other tests run correctly:

  • Remove any of the two expectations. It doesn't matter which of two it is as long as there is only one.
  • Renaming the method to testZ. This way it's alphabetically after the other tests in the same file; thus, executed last and doesn't seem to affect them anymore.

As the setExpectationOrderMatters:YES does not seem to work I tried to check the order myself doing this:

- (void)testCreatesMessagesWithCorrectHTMLForcing {
    id messageClassMock = OCMClassMock([MyMessage class]);
    __block BOOL firstInvocation = YES;
    [[[messageClassMock expect] andForwardToRealObject] messageForDictionary:[OCMArg any]
                                                          forceHTMLRendering:[OCMArg checkWithBlock:^BOOL (id obj) {
        NSNumber *boolNumber = obj;
        expect([boolNumber boolValue]).to.equal(firstInvocation);
        firstInvocation = NO;
        return YES;
    }]
                                                                   inContext:[OCMArg any]];
    NSMutableDictionary *mockJSON = [self.mockJSON mutableCopy];
    MyThread *classUnderTest = [MyThread threadForDictionary:mockJSON
                                                                       inContext:self.mockContext];
    expect(firstInvocation).to.equal(NO);
    OCMVerifyAll(messageClassMock);
    [messageClassMock stopMocking];
}

But the checkWithBlock: does not seem to be called. (The test fails at expect(firstInvocation).to.equal(NO);)

What's going on here?

Is there another (better?) way to write a test with OCMock that checks whether the method is called with the correct parameters in the correct order?

Joachim Kurz
  • 2,875
  • 6
  • 23
  • 43
  • What also seems to help is leaving out the `[messageClassMock setExpectationOrderMatters:YES];`. However, doing that it means my test will pass no matter what order the method is called in as long as it's called once with `YES` and once with `NO`. – Joachim Kurz Feb 24 '16 at 17:08

1 Answers1

3

I finally got the first solution to work. The problem is that OCMock throws an exception if expectationOrderMatters is YES. Due to the exception the test is prematurely exited and the stopMocking is never called which leads to the mock not being cleaned up properly. Subsequent calls to the mocked method then fail with the same exception making all tests fail.

The solution is to ensure stopMocking is called even if everything goes wrong. I achieved this by using try-catch like this (the change to macros and using andReturn instead of andForwardToRealObject do not matter):

MyMessage *message = [MyMessage insertInManagedObjectContext:self.mockContext];
id messageClassMock = OCMStrictClassMock([MyMessage class]);
@try {
    [messageClassMock setExpectationOrderMatters:YES];
    OCMExpect([messageClassMock messageForDictionary:[OCMArg any]
                                  forceHTMLRendering:NO
                                           inContext:[OCMArg any]]).andReturn(message);
    OCMExpect([messageClassMock messageForDictionary:[OCMArg any]
                                  forceHTMLRendering:YES
                                           inContext:[OCMArg any]]).andReturn(message);
    MyThread *classUnderTest = [MyThread threadForDictionary:self.mockJSON
                                                                       inContext:self.mockContext];
    OCMVerifyAll(messageClassMock);
}
@catch (NSException *exception) {
    XCTFail(@"An exception occured: %@", exception); // you need this, otherwise the test will incorrectly be green.
}
@finally {
    [messageClassMock stopMocking];
}

Notice the XCTFail in the catch-block: You need to include this, otherwise your test will be green although an exception occurred.

Joachim Kurz
  • 2,875
  • 6
  • 23
  • 43