4

是否有一个 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 构造,也许staticvars 有特定的内存区域?我对可以在 ObjC 中模拟类变量并且也可以进行测试的其他构造感兴趣。

4

2 回答 2

4

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

这也不是一个 Objective-C 变量,而是一个 C 变量,所以我怀疑 Objective-C 运行时中是否有任何可以提供帮助的东西。

于 2013-03-27T10:38:28.663 回答
1

简短的回答是不可能从另一个文件访问静态变量。这与试图从其他地方引用函数局部变量完全相同。该名称不可用。在 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 和您的单元测试需要查看您的类符合的声明协议。麻烦。您当然可以使该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++ 意义上的。

于 2013-03-29T20:17:46.260 回答