Running more than one unit test of GlobalActor always fails. How to unit test GlobalActor?

Unit testing of this trivial GlobalActor always fails when I run more than one unit test, but always success (I expect the test to pass) when just running once.


final actor MyGlobalActor: GlobalActor {
    static let shared = MyGlobalActor()
    
    public var isNew: Bool = true
    private init() {}
    func isNotNew() async {
        self.isNew = false
    }
}

final class GlobalActorTests: XCTestCase {
    
    func test_0() async {
        await doTest()
    }
    
    
    func test_1() async {
       await doTest()
    }
    
    func doTest() async {
        var isNew = await MyGlobalActor.shared.isNew
        XCTAssertTrue(isNew)
        await MyGlobalActor.shared.isNotNew()
        isNew = await MyGlobalActor.shared.isNew
        XCTAssertFalse(isNew)
    }
}

How can I... "reset" or otherwise "tearDown" the GlobalActor between each unit test? What are the recommendations for getting these trivial tests to pass?

1 Like

I guess I will have to put my otherwise private init behind SPI or make it internal and test not the shared but rather new instance of the actor….

2 Likes

Global actors are global: the same instance is reused for all your tests run from the same process. It's indeed difficult to isolate tests when they share mutable state :grimacing:

In your test cases, did you try overriding setUp(completion:), and reset the global actor to a pristine state before each test (I did not run the following code but you get the idea)?

final actor MyGlobalActor: GlobalActor {
    func reset() {
        isNew = true
    }
}

final class GlobalActorTests: XCTestCase {
    override func setUp() async throws {
        await MyGlobalActor.shared.reset()
    }

    func test_0() async { ... }
    ...
}

Also, I guess you should disable parallel testing.

I think that you might be asking for the impossible. Declaring the initialiser of MyGlobalActor as private is a design decision. That decision is now constraining the type of tests that you can write.

The private initialiser means that there can only be one instance of MyGlobalActor, so you can only test the effect of the isNotNew on one occasion. Even this is fraught with danger due to test parallelisation and random ordering.

This is unfortunately not an option, since this was a complete simplification of my rather much more complex actor, which contains Buffered AsyncSequence, I want to check how they behave from "fresh start".

I was hoping I could somehow use some XCTest API I did not not about to... "restart XCTest run" completely, but I guess that is not possible..?

And this unit test is in a SPM testTarget, I believe parallelisation is disabled by default for those?

As far as I know, multiple XCTestCase subclasses can be run in parallel, but the test methods of each subclass will run sequentially (in alphabetical or random order).

If you moved each test method to its own subclass, and enabled the "execute in parallel" option, there might be a separate process (and global actor) for each test.

Thanks, just tried it, did not help :(

Maybe I need put each individual test in a seperate test target :/

For what it's worth that's why anything "global" isn't great for parallel execution of different "world views" like for example isolated testing of anything.

You might be better off with using normal instance actors, even if it means more "passing around things".

Thanks, yeah I retorted to using single actor instance…

1 Like