访问从单元测试中模拟类变量的静态变量

时间:2013-03-27 10:28:21

标签: objective-c c scope objective-c-runtime

是否有Objective-C运行时库函数(不太可能)或能够检查Objective-C中的静态(准类级别)变量的函数集?我知道我可以使用类访问器方法,但我希望能够在不编写代码“测试框架”的情况下进行测试。

,是否有一种模糊的普通C技术用于外部访问静态变量?请注意,此信息仅用于单元测试目的 - 它不一定适合生产使用。我意识到这违背了静态变量的意图......一位同事提出了这个主题,我总是对挖掘ObjC / C内部结构感兴趣。

@interface Foo : NSObject
+ (void)doSomething;
@end

@implementation Foo
static BOOL bar;
+ (void)doSomething
{
  //do something with bar
}
@end

鉴于以上情况,我可以使用运行时库或其他C接口来检查bar吗?静态变量是一个C构造,也许static变量有特定的内存区域?我对可能在ObjC中模拟类变量的其他构造感兴趣,也可以进行测试。

2 个答案:

答案 0 :(得分:4)

不,不是真的,除非您通过某种类方法或其他方法公开static变量。您可以提供+ (BOOL)validateBar方法来执行您需要的任何检查,然后从您的测试框架中调用它。

这也不是Objective-C变量,而是C变量,所以我怀疑Objective-C Runtime中有什么可以帮助。

答案 1 :(得分:0)

简短的回答是accessing a static variable from another file是不可能的。这与尝试从其他地方引用函数局部变量完全相同;这个名字不可用。在C中,对象*有三个“可见性”阶段,称为“链接”:外部(全局),内部(仅限于单个“翻译单元” - 松散地,单个文件),以及没有“(功能本地)。当您将变量声明为static时,它会给出内部链接;没有其他文件可以通过名称访问它。你必须使用某种访问器功能来暴露它。

扩展答案是,由于我们可以做一些ObjC运行时库技巧来模拟类级变量,我们可以制作一些可以有条件地编译的通用测试代码。但这并不是特别简单。

在我们开始之前,我会注意到这仍然需要个性化实施一种方法;由于联系的限制,没有办法解决这个问题。

第一步,声明方法,一个用于设置,然后一个用于valueForKey:的集合 - 类似访问:

//  ClassVariablesExposer.h

#if UNIT_TESTING
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

#define ASSOC_OBJ_BY_NAME(v) objc_setAssociatedObject(self, #v, v, OBJC_ASSOCIATION_ASSIGN)
// Store POD types by wrapping their address; then the getter can access the
// up-to-date value.
#define ASSOC_BOOL_BY_NAME(b) NSValue * val = [NSValue valueWithPointer:&b];\
objc_setAssociatedObject(self, #b, val, OBJC_ASSOCIATION_RETAIN)

@interface NSObject (ClassVariablesExposer)

+ (void)associateClassVariablesByName;

+ (id)classValueForName:(char *)name;
+ (BOOL)classBOOLForName:(char *)name;

@end
#endif /* UNIT_TESTING */

这些方法在语义上更像是一个协议而不是一个类别。必须在每个子类中重写第一个方法,因为要关联的变量当然会有所不同,并且由于链接问题。您引用变量objc_setAssociatedObject()的实际调用必须位于声明变量的文件中。

然而,将此方法放入协议中需要为您的类添加额外的标头,因为虽然协议方法的实现必须放在主实现文件中,但ARC和您的单元测试需要查看声明class符合协议。繁琐。您当然可以使此NSObject类别符合协议,但无论如何您都需要一个存根来避免“不完整的实现”警告。我在开发这个解决方案时做了这些事情,并认为它们是不必要的。

第二组,即访问者,作为类别方法非常有效,因为它们看起来像这样:

//  ClassVariablesExposer.m

#import "ClassVariablesExposer.h"

#if UNIT_TESTING
@implementation NSObject (ClassVariablesExposer)

+ (void)associateClassVariablesByName
{
    // Stub to prevent warning about incomplete implementation.
}

+ (id)classValueForName:(char *)name
{
    return objc_getAssociatedObject(self, name);
}

+ (BOOL)classBOOLForName:(char *)name
{
    NSValue * v = [self classValueForName:name];
    BOOL * vp = [v pointerValue];
    return *vp;
}

@end
#endif /* UNIT_TESTING */

完全一般,虽然它们的成功使用取决于你从上面使用宏。

接下来,定义您的类,覆盖该设置方法以捕获您的类变量:

// Milliner.h

#import <Foundation/Foundation.h>

@interface Milliner : NSObject
// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof;
@end

// Milliner.m

#import "Milliner.h"

#if UNIT_TESTING
#import "ClassVariablesExposer.h"
#endif /* UNIT_TESTING */

@implementation Milliner
static NSString * featherType;
static BOOL waterproof;

+(void)initialize
{
    featherType = @"chicken hawk";
    waterproof = YES;
}

// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof
{
    waterproof = !waterproof;
}

#if UNIT_TESTING
+ (void)associateClassVariablesByName
{
    ASSOC_OBJ_BY_NAME(featherType);
    ASSOC_BOOL_BY_NAME(waterproof);
}
#endif /* UNIT_TESTING */

@end

确保您的单元测试文件导入该类别的标头。这个功能的简单演示:

#import <Foundation/Foundation.h>
#import "Milliner.h"
#import "ClassVariablesExposer.h"

#define BOOLToNSString(b) (b) ? @"YES" : @"NO"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        [Milliner associateClassVariablesByName];
        NSString * actualFeatherType = [Milliner classValueForName:"featherType"];
        NSLog(@"Assert [[Milliner featherType] isEqualToString:@\"chicken hawk\"]: %@", BOOLToNSString([actualFeatherType isEqualToString:@"chicken hawk"]));

        // Since we got a pointer to the BOOL, this does track its value.
        NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));
        [Milliner flipWaterproof];
        NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));

    }
    return 0;
}

我已将该项目放在GitHub上:https://github.com/woolsweater/ExposingClassVariablesForTesting

还有一点需要注意的是,您希望能够访问的每种POD类型都需要使用自己的方法:classIntForName:classCharForName:等。

虽然这很有效,但我总是喜欢和ObjC一起玩,但我觉得它可能只是太聪明了一半;如果你只有一个或两个这样的类变量,最简单的命题就​​是有条件地为它们编译访问器(制作一个Xcode代码片段)。如果你在一个类中有批次变量,那么我的代码可能只会节省你的时间和精力。

不过,也许你可以从中获得一些用处。我希望这至少是一个有趣的阅读。


*仅仅意味着“链接器已知的东西” - 函数,变量,结构等 - 而不是ObjC或C ++意义。