【问题标题】:Accessing static variables that are simulating class variables from unit tests从单元测试中访问模拟类变量的静态变量
【发布时间】:2013-03-17 09:40:26
【问题描述】:

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

【问题讨论】:

  • 检查objc/runtime.h
  • 这些变量来自 C:标签不应该被编辑。
  • 我确实检查了运行时标头,并且我经常使用其中一些函数。
  • 如果您想使用 Objective-C 运行时来查找该变量,请使用 Objective-C 来存储它——而不是普通的 C 静态变量。
  • @Jay 这是一个合乎逻辑的断言——然而,仅仅因为某些东西来自 C 并不一定意味着我们不能在 Objective-C 运行时中获得对它的增强访问。 ] 知道我不太可能得到肯定的答案,但作为专业人士,你需要问。 虽然不太可能我一直希望静态变量(C 构造)与 ObjC 运行时中的类相关联,尽管我知道这不太可能。毕竟,Objective-C 中的实例变量是自动初始化为 0 的结构的成员。Objective-C 不支持类变量,尽管具有误导性的 class_getInstanceVariable 函数。

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


【解决方案1】:

简短的回答是accessing a static variable from another file 是不可能的。这与试图从其他地方引用函数局部变量完全相同。该名称不可用。在 C 中,对象* 的“可见性”分为三个阶段,称为“链接”:外部(全局)、内部(仅限于单个“翻译单元”——松散地,单个文件)和“否”(函数本地)。当你将变量声明为static 时,它被赋予了内部链接;没有其他文件可以通过名称访问它。您必须创建某种访问器函数才能公开它。

扩展的答案是,由于我们无论如何都可以使用一些 ObjC 运行时库技巧来模拟类级变量,因此我们可以制作一些通用的仅测试代码,您可以有条件地编译。不过,这并不是特别简单。

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

第一步,声明方法,一个用于设置,然后一个用于valueForKey:-like 访问:

//  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 代码 sn-p)。如果您在一个类中有 很多 个变量,我的代码可能只会为您节省时间和精力。

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


*仅表示“链接器已知的东西”——函数、变量、结构等——不是 ObjC 或 C++ 意义上的。

【讨论】:

  • 我喜欢你的回答。它们总是那么重要。但也许这不适合作为评论,是吗?这个答案对我真的很有帮助。谢谢。
  • 感谢您的赞赏,@IsaMeg!看起来你可能已经快速连续地对我的几个答案投了赞成票。这个想法很好,但您应该知道系统might consider that fradulent voting 并自动反转投票。不过,我很高兴您发现我的帖子很有帮助。
  • 我赞成那些发布好答案的人的好答案。听起来不错。
  • 我不想劝阻您不要为好的答案点赞,只是为了让您了解在很短的时间内同一个人对一堆帖子进行点赞的审查。
【解决方案2】:

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

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

【讨论】:

  • +1,很好的推理不是一个Objective-C变量,而是一个C变量——我知道但坚持幻想它可能会通过一些漏洞,也希望一些的老派 C 人会想出一种通过内存地址或类似技术直接访问它的方法。
猜你喜欢
  • 1970-01-01
  • 2019-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-26
  • 1970-01-01
  • 1970-01-01
  • 2017-11-28
相关资源
最近更新 更多