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 では、オブジェクトの「可視性」には 3 つの段階があります*。これは「リンケージ」と呼ばれます: 外部 (グローバル)、内部 (単一の「翻訳単位」に制限されます。大まかに言えば、単一のファイル)、および "いいえ」(関数ローカル)。変数を として宣言するとstatic、内部リンケージが与えられます。他のファイルは名前でアクセスできません。それを公開するには、何らかのアクセサ関数を作成する必要があります。

拡張された答えは、クラスレベルの変数をシミュレートするためにとにかく実行できる ObjC ランタイム ライブラリのトリックがいくつかあるため、条件付きでコンパイルできる、ある程度一般化されたテスト専用コードを作成できるということです。ただし、特に簡単ではありません。

始める前に、1 つのメソッドを個別に実装する必要があることに注意してください。リンケージの制限のため、それを回避する方法はありません。

ステップ 1、メソッドを宣言します。1 つはセットアップ用、次に の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カテゴリをプロトコルに準拠させることはできますが、「不完全な実装」の警告を回避するには、とにかくスタブが必要です。このソリューションの開発中にこれらのことをそれぞれ行い、不要であると判断しました。

2 番目のセットであるアクセサーは、次のように見えるため、カテゴリ メソッドとして非常にうまく機能します。

//  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

もう 1 つの注意点は、アクセスできるようにする各 POD タイプにはclassIntForName:classCharForName:、 などの独自のメソッドが必要になることです。

これは機能し、私は常に ObjC をいじるのを楽しんでいますが、単純に巧妙すぎるかもしれません。これらのクラス変数が 1 つまたは 2 つしかない場合、最も簡単な提案は、それらのアクセサーを条件付きでコンパイルすることです (Xcode コード スニペットを作成します)。ここでの私のコードは、1 つのクラスに多くの変数がある場合にのみ、おそらく時間と労力を節約できます。

とはいえ、もしかしたら使い道があるかもしれません。少なくとも楽しい読書だったと思います。


*単に「リンカに知られているもの」を意味します -- 関数、変数、構造体など -- ObjC または C++ の意味ではありません。

于 2013-03-29T20:17:46.260 に答える