我该如何在Objective-C中对方法内部的对象进行单元测试?

6
我想知道如何测试这个方法。我有一个带参数的方法,根据参数的某些属性创建另一个对象并对其进行操作。代码大致如下:
- (void) navigate:(NavContext *)context {
  Destination * dest = [[Destination alloc] initWithContext:context];
  if (context.isValid) {
    [dest doSomething];
  } else {
    // something else
  }
  [dest release];
}

我想验证的是,如果context.isValid为真,则在dest上调用doSomething方法,但我不知道如何使用OCMock或任何其他传统测试方法来测试(或者是否可能),因为该对象完全在方法的范围内创建。我这样做有问题吗?
4个回答

5
你可以使用OCMock,但你需要修改代码,要么采用一个Destination对象,要么使用一个单例对象,你可以先将其替换为你的模拟对象。
最干净的方法可能是实现一个
-(void) navigate:(NavContext *)context destination:(Destination *)dest;

方法。更改-(void) navigate:(NavContext *)context的实现如下:

- (void) navigate:(NavContext *)context {
    Destination * dest = [[Destination alloc] initWithContext:context];
    [self navigate:context destination:dest];
    [dest release];
}

这将允许您的测试直接调用带有额外参数的方法。(在其他语言中,您只需为目标参数提供默认值即可实现此功能,但Objective-C不支持默认参数。)

这似乎是最简单的答案,但是当目标对象不需要在其他地方使用时,在多个作用域中存在可能会有些混乱,只是为了支持简单的测试。我想妥协总是需要做出的 :) - Kevlar
如果你想要能够替换一个单独的目标对象,那么你必须有一些传递它的方式。这是我知道的最干净的方法。 - BJ Homer

1
我想要验证的是,如果context.isValid为真,则在dest上调用doSomething。
我认为你可能在这里测试了错误的东西。您可以安全地假设(希望如此)ObjC中的布尔语句正确工作。不是您想要测试Context对象吗?如果context.isValid,那么您保证执行[dest doSomething]分支。

我正在模拟上下文对象,因为在这个测试中我不关心它是如何创建的,我关心该方法是否根据上下文属性执行正确的操作。 - Kevlar
此外,如果navigate的实现发生了改变,他仍然希望验证是否调用了doSomething。 - BJ Homer
好的,我有点短视。我喜欢BJ Homer的方法。 - EightyEight

0

我喜欢在这种情况下使用工厂方法。

@interface Destination(Factory)
+ (Destination *)destinationWithContext:(NavContext *)context;
@end

@implementation Destination(Factory)
+ (Destination *)destinationWithContext:(NavContext *)context
{
    return [[Destination alloc] initWithContext:context];
}
@end

我接着创建了一个虚假类:
#import "Destination+Factory.h"

@interface FakeDestination : Destination
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
// Note! Instance method!
- (Destination *)destinationWithContext:(NavContext *)context;
@end

@implementation FakeDestination
+ (id)sharedInstance
{
    static id _sharedInstance = nil;
    if (!_sharedInstance)
    {
        _sharedInstance = [[FakeDestination alloc] init];
    }
    return _sharedInstance;
}
+ (void)setSharedInstance:(id)sharedInstance
{
    _sharedInstance = sharedInstance;
}
// Overrides
+ (Destination *)destinationWithContext:(NavContext *)context { [FakeDestination.sharedInstance destinationWithContext:context]; }
// Instance
- (Destination *)destinationWithContext:(NavContext *)context { return nil; }
@end

一旦您设置好了这个,您只需要调整类方法 + (Destination *)destinationWithContext:(NavContext *)context;

现在您已经准备好了:

id destinationMock = [OCMock mockForClass:FakeDestination.class];
// do the swizzle
[FakeDestination setSharedInstance:destinationMock];
[[destinationMock expect] doSomething];
// Call your method
[destinationMock verify];

这需要一定的编码工作,但非常可重用。


0

完全可以使用一些有趣的技巧,比如方法交换,但这可能是错误的方式。如果绝对没有办法观察从单元测试中调用doSomething的效果,那么它调用doSomething的事实不就是一个实现细节吗?

(如果您要进行此测试,实现您目标的一种方法是替换DestinationdoSomething方法,使其通知您的单元测试,然后将调用传递给doSomething。)


我想如果我不测试方法是否被调用,这并不是什么大问题。我对TDD/单元测试还比较新,所以我还不知道所有的最佳实践 :) - Kevlar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接