Closed1

Xcode 16 beta6で遭遇したXCTestの初期化失敗エラーを回避

山田良治山田良治

Xcode 16 beta6でコンパイラの処理が変わったのか、beta4では通ってた処理が通らなくなった。

@MainActor MyTests: XCTestCaseのようにして、@MainActorを付与したオブジェクトのテストを行なっていたが、それらのsetupWithErrorでの初期化が全部隔離されたアクターコンテキスト外からのアクセスとしてコンパイルエラーになった。

@MainActor MyTests: XCTestCase {
    var object: MainActorObject!

    override func setUpWithError() throws {
        object = Object(...) // ❌ Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.  Main actor-isolated property 'object' can not be mutated from a nonisolated context.
    }
    override func tearDownWithError() throws {
        object.reset() // ❌ Main actor-isolated property 'object' can not be mutated from a nonisolated context.
    }
}

手元で試したところ、下記のような継承関係の場合、サブクラスのeat()nonisolatedなままであり、継承先では全てのシンボルがアタッチしたグローバルアクターに隔離されるわけではなく、継承元にnonisolatedなメソッドがあった場合、そのまま引き継ぐ挙動になっていることが分かった。

class MyObject {
    func eat() { }
}

@MainActor
class SubObject: MyObject {
    override func eat() {}
    func speak() { }
}

ただ気になるのは、Xcode16 beta4までは何の問題なく動いていたこと。
この継承の仕様が今入ったのか、もしくはbeta4の挙動が不正であり、バグとして修正されたのかどうなのか。

どのみち、setUpとtearDownをoverrideでアクター隔離する方法がないXCTestCaseはSwift Concurrencyとだいぶ相性が悪いと思う。Swift Testingならアクター隔離もお手のものなので、まったく問題にならない。

いまとりあえず全部処理を移行するのは大変なので、ワークアラウンドの処理を書いた。こんな感じで、全部MainActor.assumeIsolated内で記述しちゃう。

@MainActor MyTests: XCTestCase, @unchecked Sendable {
    var object: MainActorObject!

    override func setUpWithError() throws {
        MainActor.assumeIsolated {
            object = Object(...) // ✅ OK
        } 
    }
    override func tearDownWithError() throws {
        MainActor.assumeIsolated {
            object.reset() // ✅ OK
        } 
    }
}

ワークアラウンドではない正式な解決法としては、やはりSwift Testingに移行してアクター隔離されたinit内で初期化を行うことだと思う。でもこうなってくると、XCTestの存在価値が一気になくなったようにも思う。

このスクラップは2024/08/24にクローズされました