Access to static variables that model class variables from unit tests

Is there an Objective-C runtime library function (unlikely) or a set of functions capable of checking static (quasi-classical level) variables in Objective-C? I know that I can use the class access method, but I would like to pass the test without writing code "for the test environment".

Or, is there an obscure simple C technique for external access to static vars? Please note that this information is intended for unit testing - it should not be suitable for use in production. I acknowledge that this is contrary to the intentions of static wars ... a colleague touched on this topic, and I am always interested in breaking into ObjC / C internal objects.

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

Given the above, can I use the runtime library or another C interface to check for bar ? Static variables are a C construct, maybe there is a specific memory zone for static vars? I am interested in other constructs that can model class variables in ObjC and can also be tested.

+4
source share
2 answers

No, actually, unless you expose this variable to static some class method. You can provide the + (BOOL)validateBar method, which performs all the necessary checks and then calls this from your test environment.

Also, this is not an Objective-C variable, but rather a C variable, so I doubt that something useful might come up in Objective-C Runtime.

+4
source

The short answer is that accessing a static variable from another file is not possible. This is exactly the same problem as trying to refer to a local function variable from another place; the name is simply unavailable. In C there are three stages of "visibility" for objects *, which are called "binding": external (global), internal (limited to one "translation unit" - free, one file) and "no" (local function). When you declare a variable as static , it gives an internal binding; no other file can access it by name. You need to make some kind of access function to expose it.

The extended answer is that since there is some streamlining of the ObjC library that we can do in any case to simulate class-level variables, we can make a somewhat generalized test code that you can conditionally compile. However, this is not particularly clear.

Before we begin, we note that this still requires an individual implementation of one method; There is no way around this due to communication restrictions.

Step one, declare methods, one to configure, and then set for valueForKey: -like access:

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

These methods are semantically more like a protocol than a category. The first method must be overridden in each subclass, because the variables you want to bind will of course be different, and because of the binding problem. The actual call to objc_setAssociatedObject() , where you are referencing the variable, must be in the file in which the variable is declared.

However, using this method in the protocol will require an additional header for your class, because although the implementation of the protocol method should go in the main implementation file, ARC and your unit tests should see a declaration that your class complies with the protocol. Bulky. Of course, you can make this NSObject category compatible with the protocol, but then you will need a stub anyway to avoid the "incomplete implementation" warning. I did each of these steps in developing this solution and decided that they were not needed.

The second set, accessors, work perfectly as category methods, because they look like this:

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

Completely general, although their successful use depends on your use of macros from above.

Then define your class by overriding this setting method to capture class variables:

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

Make sure your unit test file imports the title for the category. A simple demonstration of this functionality:

 #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; } 

I put the project on GitHub: https://github.com/woolsweater/ExposingClassVariablesForTesting

Another caveat is that for each type of POD you want to access, you will need its own method: classIntForName: classCharForName: etc.

Although it works, and I always enjoy the monkey with ObjC, I think it might be too smart in half; if you have only one or two of these class variables, the simplest suggestion is to conditionally compile accessors for them (make an Xcode code fragment). My code here will probably only save you time and effort if you have many variables in the same class.

However, perhaps you can benefit from this. Hope this was fun to read, at least.


* The meaning is simply β€œthe thing that the linker knows” - function, variable, structure, etc. - not in the feelings of ObjC or C ++.

0
source

All Articles