Question

- (BOOL)coolMethod:(NSString*)str
{
     //do some stuff
     Webservice *ws = [[WebService alloc] init];
     NSString *result = [ws startSynchronous:url];
     if ([result isEqual:@"Something"])
     {
         //More calculation
         return YES;
     }
     return NO;
}

I am using OCUnit In the following method how can i mock my WebService Object, or the result to the method "startSynchronous" to be able to write an independent unit test?

Is it possible to inject some code in there to either create a mock web service or return a mock data on startSynchronous call?

Was it helpful?

Solution

One way is to use categories and override methods you want, you can even override the init method to return a mock object:

@interface Webservice (Mock)
- (id)init;
@end

@implementation Webservice (Mock)
- (id)init
{
     //WebServiceMock is a subclass of WebService
     WebServiceMock *moc = [[WebServiceMock alloc] init];
     return (Webservice*)moc;
}
@end

The problem with this is that if you want to make the object return different results in different tests in 1 test file you cannot do that. (You can override each method once per test page)

EDIT:

This is an old question I posted, I thought I would update the answer to how I write testable code and unit test it nowadays :)

ViewController Code

@implementation MyViewController
@synthesize webService;

- (void)viewDidLoad
{
   [super viewDidLoad];

   [self.webService sendSomeMessage:@"Some_Message"];
}

- (WebService *)webService
{
   if (!_webService)
      _webService = [[WebService alloc] init];

   return _webService;
}

@end

Test Code

@implementation MyViewControllerTest

- (void)testCorrectMessageIsSentToServer
{
   MyViewController *vc = [[MyViewController alloc] init];
   vc.webService = [OCMock niceMockForClass:[WebService class]];

   [[(OCMockObject *)vc.webService expect] sendSomeMessage@"Some_Message"];
   [vc view]; /* triggers viewDidLoad */
   [[(OCMockObject *)vc.webService verify];
}

@end

OTHER TIPS

Building on top of the WebService answer from aryaxt, here's a little trick to be able to get different results in different test.

First, you need a singleton object which will be used to store the desired answer, right before the test TestConfiguration.h

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>


void MethodSwizzle(Class c, SEL orig, SEL new);

@interface TestConfiguration : NSObject


@property(nonatomic,strong) NSMutableDictionary *results;

+ (TestConfiguration *)sharedInstance;


-(void)setNextResult:(NSObject *)result
     forCallToObject:(NSObject *)object
              selector:(SEL)selector;


-(NSObject *)getResultForCallToObject:(NSObject *)object selector:(SEL)selector;
@end

TestConfiguration.m

#import "TestConfiguration.h"


void MethodSwizzle(Class c, SEL orig, SEL new) {
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
};

@implementation TestConfiguration


- (id)init
{
    self = [super init];
    if (self) {
        self.results = [[NSMutableDictionary alloc] init];
    }
    return self;
}

+ (TestConfiguration *)sharedInstance
{
    static TestConfiguration *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[TestConfiguration alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}


-(void)setNextResult:(NSObject *)result
     forCallToObject:(NSObject *)object
            selector:(SEL)selector
{
    NSString *className =  NSStringFromClass([object class]);
    NSString *selectorName = NSStringFromSelector(selector);

    [self.results setObject:result
                     forKey:[[className stringByAppendingString:@":"] stringByAppendingString:selectorName]];
}

-(NSObject *)getResultForCallToObject:(NSObject *)object selector:(SEL)selector
{
    NSString *className =  NSStringFromClass([object class]);
    NSString *selectorName = NSStringFromSelector(selector);

    return [self.results objectForKey:[[className stringByAppendingString:@":"] stringByAppendingString:selectorName]];

}



@end

Then you would define your "Mock" category to define mock methods , such as :

#import "MyWebService+Mock.h"
#import "TestConfiguration.h"

@implementation MyWebService (Mock)


-(void)mockFetchEntityWithId:(NSNumber *)entityId
                           success:(void (^)(Entity *entity))success
                           failure:(void (^)(NSError *error))failure
{

    Entity *response = (Entity *)[[TestConfiguration sharedInstance] getResultForCallToObject:self selector:@selector(fetchEntityWithId:success:failure:)];

    if (response == nil)
    {
        failure([NSError errorWithDomain:@"entity not found" code:1 userInfo:nil]);
    }
    else{
        success(response);
    }
}

@end

And finally, in the tests themselves, you would swizzle the mock method in the setup , and define the expected answer in each test, before the call

MyServiceTest.m

- (void)setUp
{
    [super setUp];

    //swizzle webservice method call to mock object call
    MethodSwizzle([MyWebService class], @selector(fetchEntityWithId:success:failure:), @selector(mockFetchEntityWithId:success:failure:));  
}

- (void)testWSMockedEntity
{
    /* mock an entity response from the server */
    [[TestConfiguration sharedInstance] setNextResult:[Entity entityWithId:1]
                                      forCallToObject:[MyWebService sharedInstance]
                                               selector:@selector(fetchEntityWithId:success:failure:)];

    // now perform the call. You should be able to call STAssert in the blocks directly, since the success/error block should now be called completely synchronously.
}

Remark : in my example, the TestConfiguration uses class/selector as a key instead of object/selector. That means every object of the class will use the same answer for the selector. That is most likely your case, as webservice are often singleton. But it should be improved to an object/selector maybe using the objet's memory address instead of its class

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top