2

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).

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
Cris
  • 1,939
  • 3
  • 23
  • 37

1 Answers1

4

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.

Mike Mertsock
  • 11,825
  • 7
  • 42
  • 75
  • Thanks, that's interesting and informative. `stub:withBlock` seems to be undocumented. Slight pity that unlike the other stub methods, it only works (in my testing) on mocks, but, still, it's useful. Thanks also for your very useful outline of how to create matchers for arguments in message-sending expectations. The OCMock/OCHamcrest combo does this out of the box, which might be a useful direction for Kiwi to go in. – Cris Mar 28 '13 at 22:58
  • Thanks, great answer. Btw, in your `stub:withBlock:` example, you should be able just set the expectation on the array within the block itself, and avoid needing to use `capturedArray` at all. – Adam Sharp Apr 19 '13 at 03:56
  • Nice observation about setting the expectation within the block. I tried it out and that works well. It also works with asynchronous testing (`[[aMockObject shouldEventually] receive:]` in the example combined with plain `params[0] should` inside the block). – Mike Mertsock Apr 19 '13 at 11:49
  • @esker "easier to implement than the spy pattern" ? What are stub:withBlock better than Spy capture argument ? – onmyway133 Jun 12 '14 at 15:26