Search code examples
iosobjective-cunit-testingxctestxctestcase

Use of undeclared identifier error in my case


My code invokes a C library function:

@implementation Store
  ...
  -(void) doWork {
    // this is a C function from a library
    int data = getData(); 
    ...
  }
end

I am unit testing the above function, I want to mock the C function getData() in my test, here is my test case:

@interface StoreTests : XCTestCase {
    int mData;
    Store *store;
}
@end

@implementation StoreTests

-(void) setUp {
  [super setUp];
   mData = 0;
   store = [[Store alloc] init];
}

-(void) testDoWork {
  // this call will use the mocked getData(), no problem here.
  [store doWork];
}

// mocked getData()
int getData() {
   mData = 10; // Use of undeclared identifier 'mData', why?
   return mData;
}

...
@end

Why I get complier error: Use of undeclared identifier 'mData' inside mocked getData() function?


Solution

  • You are misunderstanding how instance methods and variables work.

    Every instance method has a variable self which references the current instance (or "current object") and a use of an instance variable, such as mData, is shorthand for accessing that variable using self, e.g self->mData, where -> is the (Objective-)C operator for field access. So your setup method written "long hand" is:

    -(void) setUp {
       [super setUp];
       self->mData = 0;
       self->store = [[Store alloc] init];
    }
    

    But where does self, the reference to the instance, itself come from? Well it's not magical, just hidden, it is passed to an instance method automatically as a hidden extra argument. At this point which switch to pseudo-code to show this. Your setup method is effectively compiled as:

    -(void) setUp withSelf:(StoreTest *)self {
       [super setUp];
       self->mData = 0;
       self->store = [[Store alloc] init];
    }
    

    and a call such as:

    StoreTests *myStoreTests = ...
    
    [myStoreTests setup];
    

    is effectively compiled as something like:

    [myStoreTests setup withSelf:myStoreTests];
    

    automatically adding the extra self argument.

    Now all the above only applies to methods, and enables them to access instance variables and methods, it does not apply to plain C functions - they have no hidden self argument and cannot access instance variables.

    The solution you mention in the answer you added of declaring mData outside of the interface:

    int mData;
    @interface StoreTests : XCTestCase {
       Store *store;
    }
    @end
    

    changes mData into a global variable, instead of being an instance variable. C functions can access global variables. However this does mean that every instance of the class shares the same mData, there is only one mData in this case rather than one for every instance.

    Making an instance variable into a global is therefore not a general solution to to issues like this, however as it is unlikely that you will have more than one instance of your StoreTests class it is a suitable solution in this case.

    You should however make one change: you can only have one global variable with a given name with a program, so your mData must be unique and is accessible by any code within your program, not just the code of StoreTests. You can mitigate this my declaring the variable as static:

    static int mData;
    

    this keeps the variable as global but only makes it visible to code within the same file as the declaration, which is probably just the code of StoreTests.

    HTH