andrewcopp
andrewcopp

Reputation: 105

Using OCMock with Unknown Number of Method Calls

I'm using OCMock in junction with GHUnit to try and recreate Graham Lee's "BrowseOverflow" project from Test-Driven iOS Development.

My understanding is to mock any object that isn't from the current class you are testing. I am working with my Question class which relies on my Answer class for some functionality.

Questsion.h

@class Answer;

@interface Question : NSObject {
    NSMutableSet *answerSet;
}

@property (retain) NSDate *date;
@property NSInteger score;
@property (copy) NSString *title;
@property (readonly) NSArray *answers;

- (void)addAnswer:(Answer *)answer;

@end

Question.m

#import "Question.h"
#import "Answer.h"

@implementation Question

@synthesize date;
@synthesize score;
@synthesize title;

- (id)init
{
    if ((self = [super init])) {
        answerSet = [[NSMutableSet alloc] init];
    }
    return self;
}

- (void)addAnswer:(Answer *)answer
{
    [answerSet addObject:answer];
}

- (NSArray *)answers
{
    return [[answerSet allObjects] sortedArrayUsingSelector:@selector(compare:)];
}

@end

Answer.h

#import <Foundation/Foundation.h>

@class Person;

@interface Answer : NSObject

@property (readwrite) NSString *text;
@property (readwrite) Person *person;
@property (readwrite) NSInteger score;
@property (readwrite, getter = isAccepted) BOOL accepted;

- (NSComparisonResult)compare:(Answer *)otherAnswer;

@end

Answer.m

#import "Answer.h"

@implementation Answer

@synthesize text;
@synthesize person;
@synthesize score;
@synthesize accepted;

- (NSComparisonResult)compare:(Answer *)otherAnswer
{
    if (accepted && !(otherAnswer.accepted)) {
        return NSOrderedAscending;
    } else if (!accepted && otherAnswer.accepted) {
        return NSOrderedDescending;
    }
    if (score > otherAnswer.score) {
        return NSOrderedAscending;
    } else if (score < otherAnswer.score) {
        return NSOrderedDescending;
    }
    return NSOrderedSame;
}

@end

I tried using OCMock to sub in for the instances of Answer when I was testing but it only worked about 10% of the time.

- (void)testHighScoreAnswerBeforeLow
{
    lowScore = [OCMockObject mockForClass:[Answer class]];
    [[[lowScore stub] andReturn:OCMOCK_VALUE((NSInteger) {4})] score];
    [question addAnswer:lowScore];

    highScore = [OCMockObject mockForClass:[Answer class]];
    [[[highScore stub] andReturn:OCMOCK_VALUE((NSInteger) {-4})] score];
    [question addAnswer:highScore];

    [[lowScore expect] compare:[OCMArg any]];

    NSArray *answers = question.answers;
    NSInteger highIndex = [answers indexOfObject:highScore];
    NSInteger lowIndex = [answers indexOfObject:lowScore];
    GHAssertTrue(highIndex < lowIndex, @"High-scoring index comes first");

    [highScore verify];
    [lowScore verify];
}

I think the problem is a conflict caused by how NSSet stores objects in memory (somewhat randomly) and the fact the OCMock checks to make sure no extra methods are called. I settled by taking OCMock out of this particular test but it seems like a bad test.

- (void)testHighScoreAnswerBeforeLow
{
    lowScore = [[Answer alloc] init];
    lowScore.score = -4;
    [question addAnswer:lowScore];

    highScore = [[Answer alloc] init];
    highScore.score = 4;
    [question addAnswer:highScore];

    NSArray *answers = question.answers;
    NSInteger highIndex = [answers indexOfObject:highScore];
    NSInteger lowIndex = [answers indexOfObject:lowScore];
    GHAssertTrue(highIndex < lowIndex, @"High-scoring index comes first");
}

Is it possible to get OCMock to play nice with sorting algorithms when you don't know how many comparisons they will need to make? If not, is this a disadvantage of using OCMock or is it poorly structured code?

Upvotes: 1

Views: 350

Answers (1)

Randall
Randall

Reputation: 14849

The problem is that you're setting an expectation on compare. If you don't care how many times it's called, you shouldn't tell the mock that you expect it to be called once.

You can use a nicemock if you don't care what unexpected methods are called on the mock objects.

You can use a stub to just return a certain value and not care how many times it's called.

Upvotes: 2

Related Questions