繁体   English   中英

目标C-单元测试和模拟对象?

[英]Objective C - Unit testing & Mocking object?

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

我正在使用OCUnit在以下方法中,如何模拟我的WebService对象或方法“ startSynchronous”的结果以能够编写独立的单元测试?

是否可以在其中注入一些代码以创建模拟Web服务或在startSynchronous调用上返回模拟数据?

一种方法是使用类别并覆盖所需的方法,甚至可以覆盖init方法以返回模拟对象:

@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

这样做的问题是,如果要使对象在1个测试文件中的不同测试中返回不同的结果,则不能这样做。 (您可以在每个测试页上一次覆盖每种方法)

编辑:

这是我发布的一个老问题,我认为我将更新如今如何编写可测试代码和对其进行单元测试的答案:)

ViewController代码

@implementation MyViewController
@synthesize webService;

- (void)viewDidLoad
{
   [super viewDidLoad];

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

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

   return _webService;
}

@end

测试代码

@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

基于aryaxt的WebService答案,这是一个小技巧,可以在不同的测试中获得不同的结果。

首先,您需要一个单例对象,该对象将用于存储所需的答案,就在测试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

然后,您将定义“模拟”类别以定义模拟方法,例如:

#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

最后,在测试本身中,您将调用setup中的模拟方法,并在调用之前定义每个测试中的预期答案。

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

备注:在我的示例中,TestConfiguration使用类/选择器作为键而不是对象/选择器。 这意味着该类的每个对象将对选择器使用相同的答案。 这很可能是您的情况,因为Web服务通常是单例的。 但是应该使用对象的内存地址而不是其类将其改进为对象/选择器

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM