簡體   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