0

How to write a unit test using OCMock for a complex methods which is having so many "if conditions" and the contions are checking for array count. Here is my sample method:

- (BOOL)someMethod
{
    BOOL bUploadingLocation = false;
    CLLocation *newLocation = nil;

    if([locationCallbacks count] > 0)
    {
        for(CLLocation* receivedlocation  in locationCallbacks) {
            //Print
        }
        newLocation = [locationCallbacks objectAtIndex:[locationCallbacks count] - 1];
    }
    else
    {
        return bUploadingLocation;
    }

    BOOL initialLocation = false;
    CLLocation *lastKnownLocation = [[ASAppProfileService sharedInstance] getStoredLastKnownLocation];
    if(lastKnownLocation == nil)
    {
        initialLocation = true;
    }

    BOOL checkedInstatusChange = false;
    if([[CRMgr getSharedInstance] isCREnabled])
    {
        checkedInstatusChange = [[CRMgr getSharedInstance] orchestrateCROperationsWithLocation:newLocation];
    }

    BOOL lastGoodUploadIsWithinThresold = [MELocationUtils isLastAccurateUploadWithinThresold];

    if(initialLocation  || checkedInstatusChange || !lastGoodUploadIsWithinThresold)
    {
        BOOL bCanUpload = [[ASAppProfileService sharedInstance] canUploadData];
        if(bCanUpload)
        {
            [[MEProtocolHelper sharedDataUploadManager] uploadLocationInformationPayload:newLocation];
            bUploadingLocation = true;
        }
        else
        {
            //Print something
        }
    }

    return bUploadingLocation;
}

How to write unit test for these kind of methods?

chandvoid
  • 133
  • 1
  • 12

2 Answers2

2

As you note, there are a lot of different conditions that govern the output of this method. You'll need to plan on writing several tests (I would write one test per condition).

In my setup method I would create mocks for all of the services you are calling out to:

ASAppProfileService
CRMgr
MELocationUtils
MEProtocolHelper

These mocks will be handy in each test, so no sense in setting them up in a per test basis.

You could also consider wrapping these services in instance or class methods calls:

+ (BOOL)isCheckedInStatusChange
{
    BOOL checkedInStatusChange = NO;
    if([[CRMgr getSharedInstance] isCREnabled])
    {
        checkedInStatusChange = [[CRMgr getSharedInstance] orchestrateCROperationsWithLocation:newLocation];
    }
    return checkedInStatusChange;
}

That would could make mocking easier.

Ultimately, though, there is not shortcut to testing each possible set of inputs and verifying the outputs.

Ben Flynn
  • 18,524
  • 20
  • 97
  • 142
2

To add to @Ben_Flynn 's answer, the way you design your class is going to impact how easy it is to test using mocks. In your example, you have made a few design decisions that are going to work against you. These include:

  • Using many direct references to singletons (Ex. [ASAppProfileService sharedInstance])
  • Relying on class methods rather than instance methods. (Ex. [MELocationUtils isLastAccurateUploadWithinThresold])

Singletons and class methods are notoriously difficult to mock. It can be done if you are extremely knowledgeable of some of Objective-C's more obscure language features, but it's usually a better idea to re-design your class to be more easily testable to begin with. Here are some suggestions for how to do that:

When using singletons, define properties (@property) on your class that can be used by your test code to replace the singleton with a mock during testing. There are many ways to implement this, but here's one approach I often use...

In your header file (ex. "MyThing.h")

@interface MyThing : NSObject

@property (strong, nonatomic) ASAppProfileService *profileService;

@end

In your implementation file (ex. "MyThing.m")

@implementation MyThing

- (id)init
{
    if (self = [super init]) {
        // Initialize with singleton by default.
        _profileService = [ASAppProfileService sharedInstance];
    }
    return self;
}

- (BOOL)someMethod
{
    CLLocation *lastKnownLocation = [self.profileService getStoredLastKnownLocation];
    …
}

@end

In your unit test:

- (void)test_someMethod
{
    id mockProfileService = [OCMockObject mockForClass:[ASAppProfileService class]];

    /* 
    TODO: Use [mockProfileService stub] to stub a return value 
    for the getStoredLastKnownLocation method.
    */

    MyThing *thing = [[MyThing alloc] init];

    // Replace the default value of profileService with our new mock.
    thing.pofileService = mockProfileService;

    // Exercise your object under test.
    BOOL result = [thing someMethod];

    /*TODO: Add your assertions here. */
}

The approach illustrated above is a design pattern called Dependency Injection. I recommend you read up on that design pattern. It's incredibly helpful in creating easily testable code and also has the pleasant side effect of reducing coupling between your classes which makes your code easier to change and more resistant to bugs.

Kris Schultz
  • 606
  • 1
  • 5
  • 9