【问题标题】:Objective C - Unit testing & Mocking object?Objective C - 单元测试和模拟对象?
【发布时间】:2011-02-03 01:01:30
【问题描述】:
- (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 调用时返回模拟数据?

【问题讨论】:

    标签: objective-c unit-testing mocking ocmock


    【解决方案1】:

    一种方法是使用类别和覆盖你想要的方法,你甚至可以覆盖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
    

    【讨论】:

    • 难道你不能有一个单例对象,例如“TestConfiguration”,你可以从你的 Mock 类别中为每个调用获取所需的假结果吗?
    • 您可以将任何内部结构作为属性公开,因此您可以根据需要配置您的类。
    【解决方案2】:

    基于 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 服务通常是单例的。但它应该改进为一个对象/选择器,可能使用对象的内存地址而不是它的类

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-11
      • 2014-06-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多