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

发布于 2024-10-15 10:36:09 字数 457 浏览 1 评论 0原文

- (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 调用时返回模拟数据?

- (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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

穿越时光隧道 2024-10-22 10:36:09

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

测试代码

@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

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
多孤肩上扛 2024-10-22 10:36:09

构建在 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

然后,您将定义“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 使用类/选择器作为键而不是对象/选择器。这意味着该类的每个对象都将使用相同的选择器答案。这很可能是你的情况,因为网络服务通常是单例的。但它应该改进为对象/选择器,可能使用对象的内存地址而不是它的类

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文