8

With Swift, a singleton initializer is called twice when running XCTest unit tests.

No problems with Objective-C, though, the init() method is only called once, as expected.

Here's how to build the two test projects:

Objective-C

Singleton Class

Create an empty Objective-C Project with tests. Add following bare-bones singleton:

#import "Singleton.h"

@implementation Singleton

+ (Singleton *)sharedInstance
{
    static Singleton *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[Singleton alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", self);
    }
    return self;
}
@end

AppDelegate

In the application delegate add a call to the singleton like this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [Singleton sharedInstance];
    return YES;
}

XCTestCase

Also add a call to the singleton to the generated test class:

- (void)testExample {
    [Singleton sharedInstance];
    // This is an example of a functional test case.
    XCTAssert(YES, @"Pass");
}

Results

If you add a breakpoint to the singleton's init method and run the tests, the breakpoint will only be hit once, as expected.

Swift

Now do the create a new Swift project and do the same thing.

Singleton

Create a singleton, add the test target to its Target Memberships

class Singleton {
    class var sharedInstance : Singleton {
        struct Static {
            static var onceToken : dispatch_once_t = 0
            static var instance : Singleton? = nil
        }
        dispatch_once(&Static.onceToken) {
            Static.instance = Singleton()
        }
        return Static.instance!
    }

    init() {
        NSLog("\(self)")
    }
}

AppDelegate

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    Singleton.sharedInstance
    return true
}

XCTestCase

func testExample() {
    // This is an example of a functional test case.
    Singleton.sharedInstance
    XCTAssert(true, "Pass")
}

Results

This time, if you add a breakpoint to the singleton's init method and run the tests, the breakpoint will be hit twice, first from the app delegate, then from the test case, i.e. you'll have two instances of the singleton.

Am I missing anything?

Community
  • 1
  • 1
stackguy
  • 113
  • 1
  • 6
  • It's not concerned about the question, but you might want to see this question: http://stackoverflow.com/questions/24024549/dispatch-once-singleton-model-in-swift – rintaro Nov 27 '14 at 14:31

1 Answers1

15

Since application module and tests module are separated modules, when you add the Singleton.swift file to test target member, YourApp.Singleton and YourAppTest.Singleton are not same class. That's why init called twice.

Instead of that, you should import your main module in your test file:

import YourAppName

func testExample() {
    // This is an example of a functional test case.
    Singleton.sharedInstance
    XCTAssert(true, "Pass")
}

and your Singleton class must be declared as public. see Swift, access modifiers and unit testing

public class Singleton {
    public class var sharedInstance : Singleton {
        struct Static {
            static var onceToken : dispatch_once_t = 0
            static var instance : Singleton? = nil
        }
        dispatch_once(&Static.onceToken) {
            Static.instance = Singleton()
        }
        return Static.instance!
    }

    init() {
        NSLog("\(self)")
    }
}

Don't forget to remove Singleton.swift from test target membership.

Community
  • 1
  • 1
rintaro
  • 51,423
  • 14
  • 131
  • 139
  • 2
    Wow wish I'd seen this 2 hours ago – ChrisH Oct 16 '15 at 10:53
  • YES! My issue was that I had my singleton added to both targets, thus it was able to create separate singleton instances ... thanks! – Chris Allinson Jan 24 '18 at 17:16
  • I have all of the following setup correctly, and the issue remains. Weird thing is that my project builds fine, but the failure only happens when I try to render the view on my project's first SwiftUI file. Very weird. Build failure is on my import of my appname in the test, and build error references this post. – TheJeff Feb 27 '21 at 02:48