使用OCMock对复杂方法进行单元测试

时间:2013-07-13 17:58:01

标签: ios ocmock

如何使用OCMock编写一个单元测试,用于复杂的方法,这些方法有很多“if conditions”和contions正在检查数组计数。这是我的示例方法:

- (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;
}

如何为这些方法编写单元测试?

2 个答案:

答案 0 :(得分:2)

正如您所注意到的,有许多不同的条件来控制此方法的输出。你需要计划编写几个测试(我会为每个条件写一个测试)。

在我的setup方法中,我会为您呼叫的所有服务创建模拟:

ASAppProfileService
CRMgr
MELocationUtils
MEProtocolHelper

这些模拟在每次测试中都很方便,因此在每次测试的基础上设置它们都没有意义。

您还可以考虑在实例或类方法调用中包装这些服务:

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

这可能会使嘲弄变得更容易。

但最终,没有捷径可以测试每组可能的输入并验证输出。

答案 1 :(得分:2)

要添加@Ben_Flynn的答案,您设计课程的方式将影响使用模拟测试的容易程度。在您的示例中,您做出了一些对您不利的设计决策。其中包括:

  • 使用许多直接引用单身人士(例如[ASAppProfileService sharedInstance]
  • 依赖于类方法而不是实例方法。 (例如[MELocationUtils isLastAccurateUploadWithinThresold]

众所周知,单身人士和班级方法难以嘲笑。如果您对Objective-C的一些更加模糊的语言特性非常了解,那么可以做到这一点,但通常更好的方法是重新设计您的类,以便更容易测试。以下是有关如何执行此操作的一些建议:

使用单例时,在类上定义属性(@property),测试代码可以使用这些属性在测试期间用模拟替换单例。有很多方法可以实现这一点,但这是我经常使用的一种方法......

在您的标题文件中(例如“MyThing.h”)

@interface MyThing : NSObject

@property (strong, nonatomic) ASAppProfileService *profileService;

@end

在您的实施文件中(例如“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

在您的单元测试中:

- (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. */
}

上面说明的方法是一种称为依赖注入的设计模式。我建议你阅读那个设计模式。它在创建易于测试的代码方面非常有用,并且还具有减少类之间耦合的令人愉快的副作用,这使得代码更容易更改并且更能抵御错误。