Цель C - Модульное тестирование и Mocking объект? - PullRequest
5 голосов
/ 03 февраля 2011
- (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, чтобы написать независимый модульный тест?

Возможно ли этовнедрить некоторый код туда, чтобы создать фиктивный веб-сервис или вернуть фиктивные данные при вызове startSynchronous?

Ответы [ 2 ]

4 голосов
/ 04 февраля 2011

Одним из способов является использование категорий и методов переопределения, которые вы хотите, вы даже можете переопределить метод 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
1 голос
/ 24 января 2013

Построение поверх ответа WebService от aryaxt, вот небольшая хитрость, чтобы иметь возможность получать разные результаты в разных тестах.

Во-первых, вам нужен одноэлементный объект, который будет использоваться для хранения желаемого ответа, прямо перед тестом 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

Тогда вы определите свою категорию "Mock"для определения фиктивных методов, таких как:

#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

И, наконец, в самих тестах вы должны использовать метод макета в настройке и определять ожидаемый ответ в каждом тесте перед вызовом

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 использует класс / селектор в качестве ключа вместо объекта / селектора.Это означает, что каждый объект класса будет использовать один и тот же ответ для селектора.Это, скорее всего, ваш случай, так как веб-сервисы часто бывают одноразовыми.Но это должно быть улучшено, чтобы объект / селектор мог использовать адрес памяти объекта вместо его класса

...