Search code examples
iosunit-testingnsbundlexctestocmock

XCTest fails when calling [NSBundle mainBundle]


I have some code that calls [NSBundle mainBundle] at some point, mainly to read/set preferences. When I unit test the method, the test fails because the test's mainBundle does not contain the file.

This is a known issue, that Apple won't fix as they consider it is not a bug:

the gist of their reply is that XCTest is working correctly by returning it’s own main bundle instead of the bundle of the test target.

Previously our codebase was using a category override on NSBundle's +mainBundle class method to work around this. But this is dangerous because the behaviour of overriding an existing method in a category is undefined.

If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime.

Actually Xcode warns you about it:

Category is implementing a method which will also be implemented by its primary class

So, what is the proper method to address the issue?


Solution

  • The simplest and cleanest way of fixing this issue is to partially mock the NSBundle class in your unit tests to return [NSBundle bundleForClass:[self class]] when you call [NSBundle mainBundle].

    You may put this in your -setup method so that it is mocked for your entire test class:

    static id _mockNSBundle;
    
    @implementation MyTests
    
    - (void)setUp
    {
      [super setUp];
    
      _mockNSBundle = [OCMockObject niceMockForClass:[NSBundle class]];
      NSBundle *correctMainBundle = [NSBundle bundleForClass:self.class];
      [[[[_mockNSBundle stub] classMethod] andReturn:correctMainBundle] mainBundle];
    }
    
    @end
    

    Nice and clean.

    [Source]