Search code examples
objective-ctddkiwiocmockitoochamcrest

How to set expectations on parameters to mocked methods in Kiwi


Using OCMockito and OCHamcrest, I can set up expectations on the arguments to mocked methods, thusly:

[verify(aMockObject) doSomething:allOf(is(instanceOf([NSArray class])), hasCountOf(3U), nil)];

There doesn't seem to be an equivalently simple way to do this using Kiwi. It is possible to capture arguments using a spy, something like:

KWCaptureSpy *spy = [aMockObject captureArgument:@selector(doSomething:) atIndex:0];
NSArray *capturedArray = spy.argument;

And then to check expectations on the captured object:

[[capturedArray should] haveCountOf:3U];

Is there a less clumsy way to do this in Kiwi?

(I'm aware I could probably use hamcrest matchers in here, but for the moment I'm exploring what Kiwi is capable of).


Solution

  • One option that I have used is stub:withBlock:

    NSArray* capturedArray; // declare this as __block if needed
    [aMockObject stub:@selector(doSomething:)
            withBlock:^id(NSArray *params) {
                capturedArray = params[0];
                // this is necessary even if the doSomething method returns void
                return nil;
            }];
    // exercise your object under test, then:
    [[capturedArray should] haveCountOf:3U];
    

    This works fine, and I find it easier to implement than the spy pattern. But your question made me wonder about expectations using message patterns. For example:

    [[[aMockObject should] receive] doSomething:myArray];
    [[[aMockObject should] receive] doSomething:any()];
    

    The first example will verify that aMockObject received the doSomething: message with an argument that isEqual:myArray. The second example will simply verify that doSomething: was sent, with no expectation about the array arugment. It would be great if we can specify some type of Matcher in the message pattern, to express that we don't care what specific array instance is sent in the message, just that it has a count of 3.

    I haven't found any examples of being able to do this, but it looks like there are some possibilities. To verify a message-sending expectation, Kiwi uses the KWMessagePattern class, specifically the matchesInvocation: and argumentFiltersMatchInvocationArguments: methods. This checks for three types of "argument filters":

    1. Literal object values (such as myArray in the example above), which are compared to the actual value sent in the message using isEqual:
    2. An object of type KWAny (such as the any() macro in the example above), which will match any argument value
    3. Objects that satisfy [KWGenericMatchEvaluator isGenericMatcher:argumentFilter], which basically means that the object responds to matches:(id)obj

    Thus, you should be able to use objects that implement matches: in message-pattern expectations to do things like verify the length of arrays sent to stubbed methods, without resorting to spys or blocks. Here's a very simple implementation: (available as a Gist)

    // A reusable class that satisfies isGenericMatcher:
    @interface SOHaveCountOfGenericMatcher : NSObject
    - (id)initWithCount:(NSUInteger)count;
    - (BOOL)matches:(id)item; // this is what KWMessagePattern looks for
    @property (readonly, nonatomic) NSUInteger count;
    @end
    
    @implementation SOHaveCountOfGenericMatcher
    - (id)initWithCount:(NSUInteger)count
    {
        if (self = [super init]) {
            _count = count;
        }
        return self;
    }
    - (BOOL)matches:(id)item
    {
        if (![item respondsToSelector:@selector(count)])
            return NO;
        return [item count] == self.count;
    }
    @end
    
    // Your spec:
    it(@"should receive an array with count 3", ^{
        NSArray* testArray = @[@"a", @"b", @"c"];
        id argWithCount3 = [[SOHaveCountOfGenericMatcher alloc] initWithCount:3];
        id aMockObject = [SomeObj nullMock];
        [[[aMockObject should] receive] doSomething:argWithCount3];
        [aMockObject doSomething:testArray];
    });
    

    It would be nice to be able to reuse Kiwi's built-in matcher classes here, but I haven't yet found out exactly how to do this.