diff --git a/Factory.xctestplan b/Factory.xctestplan index b7c4783c..cb1d3269 100644 --- a/Factory.xctestplan +++ b/Factory.xctestplan @@ -13,6 +13,7 @@ }, "testTargets" : [ { + "parallelizable" : true, "target" : { "containerPath" : "container:", "identifier" : "FactoryTests", diff --git a/Sources/FactoryTesting/ContainerTrait.swift b/Sources/FactoryTesting/ContainerTrait.swift index b9a95d76..5c8fdcc4 100644 --- a/Sources/FactoryTesting/ContainerTrait.swift +++ b/Sources/FactoryTesting/ContainerTrait.swift @@ -41,7 +41,7 @@ import Testing /// /// If you use a custom container, you have to create your own trait and container variable extensions. /// -/// That said, it's also possible to leverage this behavior in `XCTestCase` by using the `@TaskLocal` provided `withValue` method. +/// That said, it's also possible to leverage this behavior in XCTesting by inheriting `XCContainerTestCase` instead of `XCTestCase`. /// /// See the `Testing` documentation and examples in the `ParallelTests` and `ParallelXCTests` files. public struct ContainerTrait: TestTrait, SuiteTrait, TestScoping { @@ -62,12 +62,12 @@ public struct ContainerTrait: TestTrait, SuiteTrait, TestSco } public func provideScope(for test: Test, testCase: Test.Case?, performing function: () async throws -> Void) async throws { - try await Scope.$singleton.withValue(Scope.singleton.clone()) { - try await shared.withValue(container()) { - await transform?(C.shared) - try await function() - } - } + try await withContainer( + shared: self.shared, + container: self.container(), + operation: function, + transform: transform + ) } public func callAsFunction(transform: @escaping Transform) -> Self { @@ -75,7 +75,6 @@ public struct ContainerTrait: TestTrait, SuiteTrait, TestSco copy.transform = transform return copy } - } /// Provides test trait for default container diff --git a/Sources/FactoryTesting/WithContainer.swift b/Sources/FactoryTesting/WithContainer.swift new file mode 100644 index 00000000..01244ffd --- /dev/null +++ b/Sources/FactoryTesting/WithContainer.swift @@ -0,0 +1,33 @@ +import FactoryKit + +/// Asynchronously provides a new container and singleton scope for each operation. +public func withContainer( + shared: TaskLocal, + container: @autoclosure @Sendable () -> C, + operation: () async throws -> Void, + transform: ContainerTrait.Transform? = nil +) async rethrows { + try await Scope.$singleton.withValue(Scope.singleton.clone()) { + try await shared.withValue(container()) { + await transform?(C.shared) + try await operation() + } + } +} + +public typealias SynchronousTransform = @Sendable (C) -> Void + +/// Synchronous version of `withContainer` for use in non-async contexts. +public func withContainer( + shared: TaskLocal, + container: @autoclosure @Sendable () -> C, + operation: () throws -> Void, + transform: SynchronousTransform? = nil +) rethrows { + try Scope.$singleton.withValue(Scope.singleton.clone()) { + try shared.withValue(container()) { + transform?(C.shared) + try operation() + } + } +} diff --git a/Sources/FactoryTesting/XCContainerTestCase.swift b/Sources/FactoryTesting/XCContainerTestCase.swift new file mode 100644 index 00000000..680d0e81 --- /dev/null +++ b/Sources/FactoryTesting/XCContainerTestCase.swift @@ -0,0 +1,20 @@ +import FactoryKit +import XCTest + +/// Provides a unique `Container` and a singleton scope to every unit test function in the class. Mimicking the behavior of `ContainerTrait` for `swift-testing`. +open class XCContainerTestCase: XCTestCase { + + /// The optional transformation to apply to the `Container` before invoking the test. + /// Due to the nature of XCTest, this is not async and should be used for synchronous transformations only. + open var transform: (@Sendable (Container) -> Void)? + + /// Scopes the unit test function to a unique singleton scope and a `Container` instance transformed via the `transform` variable (if overridden to non-nil). + public override func invokeTest() { + withContainer( + shared: Container.$shared, + container: Container(), + operation: super.invokeTest, + transform: self.transform + ) + } +} diff --git a/Tests/FactoryTests/FactoryComponentTests.swift b/Tests/FactoryTests/FactoryComponentTests.swift index d9fe72bd..e4bf8e9a 100644 --- a/Tests/FactoryTests/FactoryComponentTests.swift +++ b/Tests/FactoryTests/FactoryComponentTests.swift @@ -1,4 +1,5 @@ import XCTest +import FactoryTesting @testable import FactoryKit let key1String = StaticString(stringLiteral: "s1") @@ -7,7 +8,7 @@ let key2String = StaticString(stringLiteral: "s2") let key3Unicode = MyStaticScalar("\u{1F600}").value let key4Unicode = MyStaticScalar("\u{1F601}").value -final class FactoryComponentTests: XCTestCase { +final class FactoryComponentTests: XCContainerTestCase { let key1 = FactoryKey(type: UUID.self, key: key1String) let key1D = FactoryKey(type: UUID.self, key: key1StringDup) @@ -16,11 +17,6 @@ final class FactoryComponentTests: XCTestCase { let key3U = FactoryKey(type: UUID.self, key: key3Unicode) let key4U = FactoryKey(type: UUID.self, key: key4Unicode) - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testScopeCache() { let cache = Scope.Cache() let scopeID = UUID() diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index 06249ab5..5b8dc9ac 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -1,23 +1,12 @@ import XCTest +import FactoryTesting @testable import FactoryKit #if canImport(SwiftUI) import SwiftUI #endif -final class FactoryContainerTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - CustomContainer.shared.reset() - } - - func testDecorators() { - CustomContainer.count = 0 - let _ = CustomContainer.shared.decorated() - XCTAssertEqual(CustomContainer.shared.count, 2) - } +final class FactoryContainerTests: XCContainerTestCase { func testPushPop() throws { let service1 = Container.shared.myServiceType() @@ -112,6 +101,14 @@ final class FactoryContainerTests: XCTestCase { } +final class FactoryCustomContainerTests: XCCustomContainerTestCase { + func testDecorators() { + CustomContainer.count = 0 + let _ = CustomContainer.shared.decorated() + XCTAssertEqual(CustomContainer.shared.count, 2) + } +} + private extension Container { var cachedCoverage: Factory { self { MyService() }.cached } var graphCoverage: Factory { self { MyService() }.graph } diff --git a/Tests/FactoryTests/FactoryContextTests.swift b/Tests/FactoryTests/FactoryContextTests.swift index 7fad9667..c195a251 100644 --- a/Tests/FactoryTests/FactoryContextTests.swift +++ b/Tests/FactoryTests/FactoryContextTests.swift @@ -1,14 +1,12 @@ import XCTest +import FactoryTesting @testable import FactoryKit -final class FactoryContextTests: XCTestCase { +final class FactoryContextTests: XCContainerTestCase { override func setUp() { super.setUp() - // start over - Container.shared.reset() - // externally defined contexts Container.shared.externalContextService .register { ContextService(name: "REGISTERED") } diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index d4a17bc0..d26a9b52 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -1,15 +1,14 @@ import XCTest +import FactoryTesting @testable import FactoryKit #if canImport(SwiftUI) import SwiftUI #endif -final class FactoryCoreTests: XCTestCase { +final class FactoryCoreTests: XCContainerAndCustomContainerTestCase { override func setUp() { - Container.shared.reset() - CustomContainer.shared.reset() CustomContainer.shared.count = 0 } @@ -193,34 +192,6 @@ final class FactoryCoreTests: XCTestCase { } } - @MainActor - func testStrictPromise() { - // Expect non fatal error when strict and NOT in debug mode - Container.shared.manager.promiseTriggersError = false - expectNonFatalError { - let _ = Container.shared.strictPromisedService() - } - // Expect fatal error when strict and in debug mode - Container.shared.manager.promiseTriggersError = true - expectFatalError(expectedMessage: "MyServiceType was not registered") { - let _ = Container.shared.strictPromisedService() - } - } - - @MainActor - func testStrictParameterPromise() { - // Expect non fatal error when strict and NOT in debug mode - Container.shared.manager.promiseTriggersError = false - expectNonFatalError { - let _ = Container.shared.strictPromisedParameterService(23) - } - // Expect fatal error when strict and in debug mode - Container.shared.manager.promiseTriggersError = true - expectFatalError(expectedMessage: "ParameterService was not registered") { - let _ = Container.shared.strictPromisedParameterService(23) - } - } - func testTrace() { var logged: [String] = [] Container.shared.manager.trace.toggle() @@ -257,3 +228,36 @@ final class FactoryCoreTests: XCTestCase { #endif } + +// FactoryContext.current is not yet using @TaskLocal therefore we cannot use safely the `XCContainerTestCase` here. +final class FactoryCoreStrictPromiseTests: XCTestCase { + + @MainActor + func testStrictPromise() { + // Expect non fatal error when strict and NOT in debug mode + Container.shared.manager.promiseTriggersError = false + expectNonFatalError { + let _ = Container.shared.strictPromisedService() + } + // Expect fatal error when strict and in debug mode + Container.shared.manager.promiseTriggersError = true + expectFatalError(expectedMessage: "MyServiceType was not registered") { + let _ = Container.shared.strictPromisedService() + } + } + + @MainActor + func testStrictParameterPromise() { + // Expect non fatal error when strict and NOT in debug mode + Container.shared.manager.promiseTriggersError = false + expectNonFatalError { + let _ = Container.shared.strictPromisedParameterService(23) + } + // Expect fatal error when strict and in debug mode + Container.shared.manager.promiseTriggersError = true + expectFatalError(expectedMessage: "ParameterService was not registered") { + let _ = Container.shared.strictPromisedParameterService(23) + } + } + +} diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index afa0dd1e..b9480fe4 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -1,13 +1,8 @@ import XCTest +import FactoryTesting @testable import FactoryKit -final class FactoryDefectTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - Scope.singleton.reset() - } +final class FactoryDefectTests: XCContainerTestCase { // scope would not correctly resolve a factory with an optional type. e.g. Factory(scope: .cached) { nil } func testNilScopedService() throws { @@ -133,37 +128,6 @@ final class FactoryDefectTests: XCTestCase { XCTAssertFalse(Container.shared.manager.isEmpty(.scope)) } - // Registration on a new container could be overridden by auto registration - func testRegistrationOverriddenByAutoRegistration() throws { - let container1 = AutoRegisteringContainer() - let service1 = container1.test() - XCTAssertEqual(service1.value, 32) - let container2 = AutoRegisteringContainer() - container2.test.register { - MockServiceN(64) - } - let service2 = container2.test() - XCTAssertEqual(service2.value, 64) - } - - // AutoRegistration should have no effect on singletons - func testAutoRegistrationAndSingletonCache() throws { - let container1 = AutoRegisteringContainer() - let service1 = container1.singletonTest() - // should be auto registration value - XCTAssertEqual(service1.value, 32) - container1.singletonTest.register { - MockServiceN(64) - } - let service2 = container1.singletonTest() - // register should have cleared singleton scope cache so should be new value - XCTAssertEqual(service2.value, 64) - let container2 = AutoRegisteringContainer() - let service3 = container2.singletonTest() - // auto registration should have no effect on scope cache - XCTAssertEqual(service3.value, 64) - } - // #114 #146 setting a context would not clear scope cache as does register func testContextClearingScope() throws { let service1 = Container.shared.cachedService() @@ -196,7 +160,39 @@ final class FactoryDefectTests: XCTestCase { XCTAssertFalse(service4.id == service5.id) XCTAssertFalse(service3.id == service5.id) } +} +final class FactoryAutoRegisteringTests: XCAutoRegisteringContainerTestCase { + // Registration on a new container could be overridden by auto registration + func testRegistrationOverriddenByAutoRegistration() throws { + let container1 = AutoRegisteringContainer() + let service1 = container1.test() + XCTAssertEqual(service1.value, 32) + let container2 = AutoRegisteringContainer() + container2.test.register { + MockServiceN(64) + } + let service2 = container2.test() + XCTAssertEqual(service2.value, 64) + } + + // AutoRegistration should have no effect on singletons + func testAutoRegistrationAndSingletonCache() throws { + let container1 = AutoRegisteringContainer() + let service1 = container1.singletonTest() + // should be auto registration value + XCTAssertEqual(service1.value, 32) + container1.singletonTest.register { + MockServiceN(64) + } + let service2 = container1.singletonTest() + // register should have cleared singleton scope cache so should be new value + XCTAssertEqual(service2.value, 64) + let container2 = AutoRegisteringContainer() + let service3 = container2.singletonTest() + // auto registration should have no effect on scope cache + XCTAssertEqual(service3.value, 64) + } } extension Container { @@ -225,17 +221,35 @@ fileprivate class LockingTestB { init() {} } -fileprivate final class AutoRegisteringContainer: SharedContainer, AutoRegistering { - static let shared = AutoRegisteringContainer() +package final class AutoRegisteringContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = AutoRegisteringContainer() + #else + package static let shared = AutoRegisteringContainer() + #endif var test: Factory { self { MockServiceN(16) } } var singletonTest: Factory { self { MockServiceN(16) }.singleton } - func autoRegister() { + package func autoRegister() { test.register { MockServiceN(32) } singletonTest.register { MockServiceN(32) } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCAutoRegisteringContainerTestCase: XCTestCase { + package var transform: (@Sendable (AutoRegisteringContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: AutoRegisteringContainer.$shared, + container: AutoRegisteringContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } } + diff --git a/Tests/FactoryTests/FactoryFunctionalTests.swift b/Tests/FactoryTests/FactoryFunctionalTests.swift index 0e514714..5f65c1a3 100644 --- a/Tests/FactoryTests/FactoryFunctionalTests.swift +++ b/Tests/FactoryTests/FactoryFunctionalTests.swift @@ -1,6 +1,7 @@ #if canImport(os) import XCTest +import FactoryTesting import os @testable import FactoryKit @@ -33,13 +34,7 @@ final class OpenURLFunctionMock: Sendable { } @available(iOS 16.0, *) -final class FactoryFunctionalTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } - +final class FactoryFunctionalTests: XCContainerTestCase { func testOpenFunctionality() throws { let openedURL: OSAllocatedUnfairLock = .init(initialState: nil) diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index 1b580dc8..c57f7efa 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -6,6 +6,7 @@ import Observation import SwiftUI #endif +import FactoryTesting @testable import FactoryKit class Services1 { @@ -112,12 +113,7 @@ extension Container { } } -final class FactoryInjectionTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } +final class FactoryInjectionTests: XCContainerTestCase { func testBasicInjection() throws { let services = Services1() diff --git a/Tests/FactoryTests/FactoryIsolationTests.swift b/Tests/FactoryTests/FactoryIsolationTests.swift index 7beb80a2..788a8d84 100644 --- a/Tests/FactoryTests/FactoryIsolationTests.swift +++ b/Tests/FactoryTests/FactoryIsolationTests.swift @@ -1,5 +1,5 @@ import XCTest - +import FactoryTesting @testable import FactoryKit private struct SomeSendableType: Sendable {} @@ -53,12 +53,7 @@ extension Container { } } -final class FactoryIsolationTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } +final class FactoryIsolationTests: XCContainerTestCase { // test resolution of sendable type @MainActor diff --git a/Tests/FactoryTests/FactoryMultithreadingTests.swift b/Tests/FactoryTests/FactoryMultithreadingTests.swift index f65a27d9..89c6c98b 100644 --- a/Tests/FactoryTests/FactoryMultithreadingTests.swift +++ b/Tests/FactoryTests/FactoryMultithreadingTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import FactoryKit -final class FactoryMultithreadingTests: XCTestCase, @unchecked Sendable { +final class FactoryMultithreadingTests: XCMultiThreadedContainerTestCase, @unchecked Sendable { let qa = DispatchQueue(label: "A", qos: .userInteractive, attributes: .concurrent) let qb = DispatchQueue(label: "B", qos: .userInitiated, attributes: .concurrent) @@ -110,7 +111,7 @@ func increment() { lock.unlock() } -fileprivate class A { +package class A { var b: B init(b: B) { self.b = b @@ -120,7 +121,7 @@ fileprivate class A { } } -fileprivate class B { +package class B { var c: C init(c: C) { self.c = c @@ -130,7 +131,7 @@ fileprivate class B { } } -fileprivate class C { +package class C { var d: D init(d: D) { self.d = d @@ -140,14 +141,14 @@ fileprivate class C { } } -fileprivate class D { +package class D { init() {} func test() { increment() } } -fileprivate class E { +package class E { @LazyInjected(\MultiThreadedContainer.d) var d: D init() {} func test() { @@ -156,12 +157,29 @@ fileprivate class E { } } -fileprivate final class MultiThreadedContainer: SharedContainer { - fileprivate static let shared = MultiThreadedContainer() +package final class MultiThreadedContainer: SharedContainer { + #if swift(>=5.5) + @TaskLocal package static var shared = MultiThreadedContainer() + #else + package static let shared = MultiThreadedContainer() + #endif fileprivate var a: Factory { self { A(b: self.b()) } } fileprivate var b: Factory { self { B(c: self.c()) } } fileprivate var c: Factory { self { C(d: self.d()) } } fileprivate var d: Factory { self { D() }.cached } fileprivate var e: Factory { self { E() } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCMultiThreadedContainerTestCase: XCTestCase { + package var transform: (@Sendable (MultiThreadedContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: MultiThreadedContainer.$shared, + container: MultiThreadedContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } } diff --git a/Tests/FactoryTests/FactoryParameterTests.swift b/Tests/FactoryTests/FactoryParameterTests.swift index 2c45b0c3..141e4e3e 100644 --- a/Tests/FactoryTests/FactoryParameterTests.swift +++ b/Tests/FactoryTests/FactoryParameterTests.swift @@ -1,16 +1,12 @@ import XCTest +import FactoryTesting @testable import FactoryKit #if canImport(SwiftUI) import SwiftUI #endif -final class FactoryParameterTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } +final class FactoryParameterTests: XCContainerTestCase { func testParameterServiceResolutions() throws { let service1 = Container.shared.parameterService(5) diff --git a/Tests/FactoryTests/FactoryResolverTests.swift b/Tests/FactoryTests/FactoryResolverTests.swift index 68cbec87..2cbf5a05 100644 --- a/Tests/FactoryTests/FactoryResolverTests.swift +++ b/Tests/FactoryTests/FactoryResolverTests.swift @@ -1,16 +1,11 @@ import XCTest +import FactoryTesting @testable import FactoryKit -final class FactoryResolverTests: XCTestCase { - - fileprivate var container: ResolvingContainer! - - override func setUp() { - super.setUp() - container = ResolvingContainer() - } +final class FactoryResolverTests: XCResolvingContainerTestCase { func testBasicResolve() throws { + let container = ResolvingContainer() let service1: MyService? = container.resolve() let service2: MyService? = container.resolve() XCTAssertNotNil(service1) @@ -20,6 +15,7 @@ final class FactoryResolverTests: XCTestCase { } func testResolvingScope() throws { + let container = ResolvingContainer() let service0: MyServiceType? = container.resolve() XCTAssertNil(service0) container.register { MyService() as MyServiceType } @@ -33,6 +29,7 @@ final class FactoryResolverTests: XCTestCase { } func testFactoryScope() throws { + let container = ResolvingContainer() container.factory(MyService.self)? .scope(.singleton) let service1: MyService? = container.resolve() @@ -45,12 +42,16 @@ final class FactoryResolverTests: XCTestCase { } -fileprivate final class ResolvingContainer: SharedContainer, AutoRegistering, Resolving { - static let shared = ResolvingContainer() - func autoRegister() { +package final class ResolvingContainer: SharedContainer, AutoRegistering, Resolving { + #if swift(>=5.5) + @TaskLocal package static var shared = ResolvingContainer() + #else + package static let shared = ResolvingContainer() + #endif + package func autoRegister() { register { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() func someService() -> MyServiceType { self { MyService() }() @@ -72,3 +73,16 @@ fileprivate final class ResolvingContainer: SharedContainer, AutoRegistering, Re } } + +package class XCResolvingContainerTestCase: XCTestCase { + package var transform: (@Sendable (ResolvingContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: ResolvingContainer.$shared, + container: ResolvingContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } +} diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index c3c51332..fac32b7c 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -1,12 +1,8 @@ import XCTest +import FactoryTesting @testable import FactoryKit -final class FactoryScopeTests: XCTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } +final class FactoryScopeTests: XCContainerTestCase { func testUniqueScope() throws { let service1 = Container.shared.myServiceType() @@ -363,6 +359,22 @@ final class FactoryScopeTests: XCTestCase { XCTAssertTrue(service3?.id == service4?.id) // should be cached } + @available(iOS 13, *) + func testSingletonScopeTimeToLive() async throws { + Container.shared.singletonService.timeToLive(0.01) + let service1 = Container.shared.singletonService() + let service2 = Container.shared.singletonService() + XCTAssertTrue(service1.id == service2.id) + // delay + try await Task.sleep(nanoseconds: 10_100_000) + // resolution should fail ttl test and return new instance + let service3 = Container.shared.singletonService() + XCTAssertTrue(service2.id != service3.id) + } + +} + +final class FactoryScopeTestsFirstSingleton: XCFirstSingletonContainerTestCase { func testSingletonSameContainerType() throws { let container1 = FirstSingletonContainer() //container1.manager.trace.toggle() @@ -375,6 +387,9 @@ final class FactoryScopeTests: XCTestCase { XCTAssertTrue(service3.id == service4.id) XCTAssertTrue(service1.id == service3.id) } +} + +final class FactoryScopeTestsFirstAndSecondSingleton: XCFirstAndSecondSingletonContainerTestCase { func testSingletonAcrossContainerTypes() throws { let container1 = FirstSingletonContainer() @@ -386,19 +401,10 @@ final class FactoryScopeTests: XCTestCase { container1.manager.trace.toggle() } - @available(iOS 13, *) - func testSingletonScopeTimeToLive() async throws { - Container.shared.singletonService.timeToLive(0.01) - let service1 = Container.shared.singletonService() - let service2 = Container.shared.singletonService() - XCTAssertTrue(service1.id == service2.id) - // delay - try await Task.sleep(nanoseconds: 10_100_000) - // resolution should fail ttl test and return new instance - let service3 = Container.shared.singletonService() - XCTAssertTrue(service2.id != service3.id) - } - +} + +final class FactoryScopeTestsCachedContainer: XCCachedContainerTestCase { + func testUniqueResolutionOnCachedContainer() throws { let service1 = CachedContainer.shared.uniqueService() let service2 = CachedContainer.shared.uniqueService() @@ -414,36 +420,96 @@ extension SharedContainer { } } -fileprivate final class FirstSingletonContainer: SharedContainer, AutoRegistering { - static let shared = FirstSingletonContainer() - func autoRegister() { +package final class FirstSingletonContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = FirstSingletonContainer() + #else + package static let shared = FirstSingletonContainer() + #endif + package func autoRegister() { manager.defaultScope = .singleton } var myServiceType: Factory { self { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() } -fileprivate final class SecondSingletonContainer: SharedContainer, AutoRegistering { - static let shared = SecondSingletonContainer() - func autoRegister() { +package class XCFirstSingletonContainerTestCase: XCTestCase { + package var transform: (@Sendable (FirstSingletonContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: FirstSingletonContainer.$shared, + container: FirstSingletonContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } +} + +package final class SecondSingletonContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = SecondSingletonContainer() + #else + package static let shared = SecondSingletonContainer() + #endif + package func autoRegister() { manager.defaultScope = .singleton } var myServiceType: Factory { self { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCFirstAndSecondSingletonContainerTestCase: XCTestCase { + package var firstTransform: (@Sendable (FirstSingletonContainer) -> Void)? + package var secondTransform: (@Sendable (SecondSingletonContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: SecondSingletonContainer.$shared, + container: SecondSingletonContainer(), + operation: { + withContainer( + shared: FirstSingletonContainer.$shared, + container: FirstSingletonContainer(), + operation: super.invokeTest, + transform: self.firstTransform + ) + }, + transform: self.secondTransform + ) + } } +package final class CachedContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared: CachedContainer = CachedContainer() + #else + package static let shared = CachedContainer() + #endif -fileprivate final class CachedContainer: SharedContainer, AutoRegistering { - static let shared: CachedContainer = CachedContainer() - let manager: ContainerManager = ContainerManager() - func autoRegister() { + package let manager: ContainerManager = ContainerManager() + package func autoRegister() { manager.defaultScope = .cached } var uniqueService: Factory { self { MyService() }.unique } } + +package class XCCachedContainerTestCase: XCTestCase { + package var transform: (@Sendable (CachedContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CachedContainer.$shared, + container: CachedContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } + +} diff --git a/Tests/FactoryTests/MockServices.swift b/Tests/FactoryTests/MockServices.swift index c72ec850..8c3c4b56 100644 --- a/Tests/FactoryTests/MockServices.swift +++ b/Tests/FactoryTests/MockServices.swift @@ -194,8 +194,12 @@ extension Container { // Custom Container -final class CustomContainer: SharedContainer, AutoRegistering { - @TaskLocal static var shared = CustomContainer() +package final class CustomContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = CustomContainer() + #else + package static let shared = CustomContainer() + #endif nonisolated(unsafe) static var count = 0 nonisolated(unsafe) var count = 0 var test: Factory { @@ -231,7 +235,7 @@ final class CustomContainer: SharedContainer, AutoRegistering { } .once() } - func autoRegister() { + package func autoRegister() { print("CustomContainer AUTOREGISTERING") Self.count = 1 self.count = 1 @@ -244,5 +248,5 @@ final class CustomContainer: SharedContainer, AutoRegistering { } #endif } - let manager = ContainerManager() + package let manager = ContainerManager() } diff --git a/Tests/FactoryTests/ParallelServices.swift b/Tests/FactoryTests/ParallelServices.swift index 22f6584f..59a60d84 100644 --- a/Tests/FactoryTests/ParallelServices.swift +++ b/Tests/FactoryTests/ParallelServices.swift @@ -7,6 +7,7 @@ import Foundation import Testing +import XCTest @testable import FactoryKit import FactoryTesting @@ -18,6 +19,60 @@ extension Trait where Self == ContainerTrait { .init(shared: CustomContainer.$shared, container: .init()) } } + +package class XCCustomContainerTestCase: XCTestCase { + package var transform: (@Sendable (CustomContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } +} + +func withContainerAndCustomContainer( + operation: @Sendable () -> Void, + containerTransform: (@Sendable (Container) -> Void)? = nil, + customContainerTransform: (@Sendable (CustomContainer) -> Void)? = nil +) { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: { + withContainer( + shared: Container.$shared, + container: Container(), + operation: operation, + transform: containerTransform + ) + }, + transform: customContainerTransform + ) +} + +package class XCContainerAndCustomContainerTestCase: XCTestCase { + package var containerTransform: (@Sendable (Container) -> Void)? + package var customContainerTransform: (@Sendable (CustomContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: { + withContainer( + shared: Container.$shared, + container: Container(), + operation: super.invokeTest, + transform: self.containerTransform + ) + }, + transform: self.customContainerTransform + ) + } +} #endif // Classes for @TaskLocal and TestTrait tests diff --git a/Tests/FactoryTests/ParallelXCTest.swift b/Tests/FactoryTests/ParallelXCTest.swift index 323b99c6..abc288fe 100644 --- a/Tests/FactoryTests/ParallelXCTest.swift +++ b/Tests/FactoryTests/ParallelXCTest.swift @@ -1,42 +1,453 @@ -#if swift(>=6.1) +#if swift(>=5.5) import XCTest +import FactoryTesting @testable import FactoryKit -final class ParallelXCTest: XCTestCase { - func testFooBarBaz() { - let container = Container() +/// Illustrates using the regular `XCTestCase` with `FactoryTesting.withContainer` to enable parallel XCTests. +final class ParallelXCTestFoo: XCTestCase { + func testFoo() { let fooExpectation = expectation(description: "foo") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Foo() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "foo") + commonTests("foo") fooExpectation.fulfill() } + wait(for: [fooExpectation], timeout: 60) + } +} + +final class ParallelXCTestBar: XCTestCase { + func testBar() { let barExpectation = expectation(description: "bar") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Bar() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "bar") + commonTests("bar") barExpectation.fulfill() } + wait(for: [barExpectation], timeout: 60) + } +} + +final class ParallelXCTestBaz: XCTestCase { + func testBaz() { let bazExpectation = expectation(description: "baz") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Baz() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } + + commonTests("baz") + bazExpectation.fulfill() + } + + wait(for: [bazExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestFooWithContainerAndTransform: XCTestCase { + func testFooWithContainerSyncTransform() { + let fooExpectation = expectation(description: "foo") + + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("foo") + fooExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Foo() } + $0.fooBarBazCached.register { Foo() } + $0.fooBarBazSingleton.register { Foo() } + } + + wait(for: [fooExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestBarWithContainerAndTransform: XCTestCase { + func testBarWithContainerSyncTransform() { + let barExpectation = expectation(description: "bar") + + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("bar") + barExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Bar() } + $0.fooBarBazCached.register { Bar() } + $0.fooBarBazSingleton.register { Bar() } + } + + wait(for: [barExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestBazWithContainerAndTransform: XCTestCase { + func testBazWithContainerSyncTransform() { + let bazExpectation = expectation(description: "baz") + + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("baz") + bazExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + } + + wait(for: [bazExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper asynchronously +final class ParallelXCTestFooWithContainerAndAsyncTransform: XCTestCase { + func testFooWithContainerAsync() async { + let fooExpectation = expectation(description: "foo") + + await withContainer( + shared: Container.$shared, + container: Container() + ) { + Container.shared.fooBarBaz.register { Foo() } + Container.shared.fooBarBazCached.register { Foo() } + Container.shared.fooBarBazSingleton.register { Foo() } + + await Container.shared.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "baz") + await Container.shared.isolatedToCustomGlobalActor.register { IsolatedFoo() } + await Container.shared.isolatedToCustomGlobalActorCached.register { IsolatedFoo() } + await Container.shared.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + await isolatedAsyncTests("foo") + fooExpectation.fulfill() + } + + await fulfillment(of: [fooExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with an asynchronous transform closure +final class ParallelXCTestBarWithContainerAndAsyncTransform: XCTestCase { + func testBarWithContainerAsyncTransform() async { + let barExpectation = expectation(description: "bar") + + await withContainer( + shared: Container.$shared, + container: Container() + ) { + await isolatedAsyncTests("bar") + barExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Bar() } + $0.fooBarBazCached.register { Bar() } + $0.fooBarBazSingleton.register { Bar() } + + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await $0.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await $0.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "bar") } + + await $0.isolatedToCustomGlobalActor.register { IsolatedBar() } + await $0.isolatedToCustomGlobalActorCached.register { IsolatedBar() } + await $0.isolatedToCustomGlobalActorSingleton.register { IsolatedBar() } + } + + await fulfillment(of: [barExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with an asynchronous transform closure +final class ParallelXCTestBazWithContainerAndAsyncTransform: XCTestCase { + func testBazWithContainerAsyncTransform() async { + let bazExpectation = expectation(description: "baz") + + await withContainer( + shared: Container.$shared, + container: Container() + ) { + await isolatedAsyncTests("baz") bazExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await $0.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await $0.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "baz") } + + await $0.isolatedToCustomGlobalActor.register { IsolatedBaz() } + await $0.isolatedToCustomGlobalActorCached.register { IsolatedBaz() } + await $0.isolatedToCustomGlobalActorSingleton.register { IsolatedBaz() } + } + + await fulfillment(of: [bazExpectation], timeout: 60) + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestFoo: XCContainerTestCase { + func testFoo() { + let c = Container.shared + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } + + commonTests("foo") + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestBar: XCContainerTestCase { + func testBar() { + let c = Container.shared + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } + + commonTests("bar") + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestBaz: XCContainerTestCase { + func testBaz() { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } + + commonTests("baz") + } +} + +/// Illustrates using the `XCContainerTestCase` with different isolations. +final class ParallelIsolatedXCTestsFoo: XCContainerTestCase { + func testIsolatedFoo() async { + let c = Container.shared + + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } + + await c.isolatedToCustomGlobalActor.register { IsolatedFoo() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedFoo() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + await isolatedAsyncTests("foo") + } +} + +/// Illustrates using the `XCContainerTestCase` with different isolations. +final class ParallelIsolatedXCTestsBar: XCContainerTestCase { + func testIsolatedBar() async { + let c = Container.shared + + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "bar") } + + await c.isolatedToCustomGlobalActor.register { IsolatedBar() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedBar() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedBar() } + + await isolatedAsyncTests("bar") + } +} + +/// Illustrates using the `XCContainerTestCase` with the transform sugar via the initalizer and with different isolations. +final class ParallelIsolatedXCTestsBaz: XCContainerTestCase { + + /// Overriding the transform property to register dependencies in the Container for every unit test inside this class. + /// This is a synchronous transform, so it should not contain any async code, unlike the `transform` in the `ContainerTrait` for `swift-testing`. + override var transform: (@Sendable (Container) -> Void)? { + get { + { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + } + } + set { } + } + + func testIsolatedBaz() async { + let c = Container.shared + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "baz") } + + await c.isolatedToCustomGlobalActor.register { IsolatedBaz() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedBaz() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedBaz() } + + await isolatedAsyncTests("baz") + } +} + +final class ParallelCustomContainerTest: XCCustomContainerTestCase { + func testCustomContainer() { + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") + } +} + +/// Illustrates using multiple containers with a custom withContainer variant +final class ParallelWithContainerAndCustomContainerTest: XCTestCase { + func testContainerAndCustomContainer() { + withContainerAndCustomContainer { + commonTests("baz") + + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") + } containerTransform: { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } } + } +} + +/// Illustrates using multiple containers with a custom XCContainerAndCustomContainerTestCase +final class ParallelContainerAndCustomContainerTest: XCContainerAndCustomContainerTestCase { + func testContainerAndCustomContainer() { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } + + commonTests("baz") - wait(for: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") } } + +private func commonTests(_ value: String) { + let sut1 = TaskLocalUseCase() + XCTAssertEqual(sut1.fooBarBaz.value, value) + XCTAssertEqual(sut1.fooBarBazCached.value, value) + XCTAssertEqual(sut1.fooBarBazSingleton.value, value) + + let sut2 = TaskLocalUseCase() + XCTAssertEqual(sut2.fooBarBaz.value, value) + XCTAssertEqual(sut2.fooBarBazCached.value, value) + XCTAssertEqual(sut2.fooBarBazSingleton.value, value) + + XCTAssertNotEqual(sut1.fooBarBaz.id, sut2.fooBarBaz.id) + XCTAssertEqual(sut1.fooBarBazCached.id, sut2.fooBarBazCached.id) + XCTAssertEqual(sut1.fooBarBazSingleton.id, sut2.fooBarBazSingleton.id) + + Container.shared.fooBarBazSingleton.register { Foo() } + + let sut3 = TaskLocalUseCase() + XCTAssertEqual(sut3.fooBarBazSingleton.value, "foo") + XCTAssertNotEqual(sut1.fooBarBazSingleton.id, sut3.fooBarBazSingleton.id) +} + +@MainActor +private func isolatedAsyncTests(_ value: String) async { + let sut1 = await IsolatedTaskLocalUseCase() + + XCTAssertEqual(sut1.fooBarBaz.value, value) + XCTAssertEqual(sut1.fooBarBazCached.value, value) + XCTAssertEqual(sut1.fooBarBazSingleton.value, value) + + XCTAssertEqual(sut1.isolatedToMainActor.value, value) + XCTAssertEqual(sut1.isolatedToMainActorCached.value, value) + XCTAssertEqual(sut1.isolatedToMainActorSingleton.value, value) + + XCTAssertEqual(sut1.isolatedToCustomGlobalActor.value, value) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorCached.value, value) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorSingleton.value, value) + + let sut2 = await IsolatedTaskLocalUseCase() + XCTAssertEqual(sut2.fooBarBaz.value, value) + XCTAssertEqual(sut2.fooBarBazCached.value, value) + XCTAssertEqual(sut2.fooBarBazSingleton.value, value) + + XCTAssertEqual(sut2.isolatedToMainActor.value, value) + XCTAssertEqual(sut2.isolatedToMainActorCached.value, value) + XCTAssertEqual(sut2.isolatedToMainActorSingleton.value, value) + + XCTAssertEqual(sut2.isolatedToCustomGlobalActor.value, value) + XCTAssertEqual(sut2.isolatedToCustomGlobalActorCached.value, value) + XCTAssertEqual(sut2.isolatedToCustomGlobalActorSingleton.value, value) + + XCTAssertNotEqual(sut1.fooBarBaz.id, sut2.fooBarBaz.id) + XCTAssertEqual(sut1.fooBarBazCached.id, sut2.fooBarBazCached.id) + XCTAssertEqual(sut1.fooBarBazSingleton.id, sut2.fooBarBazSingleton.id) + + XCTAssertNotEqual(sut1.isolatedToMainActor.id, sut2.isolatedToMainActor.id) + XCTAssertEqual(sut1.isolatedToMainActorCached.id, sut2.isolatedToMainActorCached.id) + XCTAssertEqual(sut1.isolatedToMainActorSingleton.id, sut2.isolatedToMainActorSingleton.id) + + XCTAssertNotEqual(sut1.isolatedToCustomGlobalActor.id, sut2.isolatedToCustomGlobalActor.id) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorCached.id, sut2.isolatedToCustomGlobalActorCached.id) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorSingleton.id, sut2.isolatedToCustomGlobalActorSingleton.id) + + Container.shared.fooBarBazSingleton.register { Foo() } + Container.shared.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + let sut3 = await IsolatedTaskLocalUseCase() + XCTAssertEqual(sut3.fooBarBazSingleton.value, "foo") + XCTAssertEqual(sut3.isolatedToMainActorSingleton.value, "foo") + XCTAssertEqual(sut3.isolatedToCustomGlobalActorSingleton.value, "foo") + + XCTAssertNotEqual(sut1.fooBarBazSingleton.id, sut3.fooBarBazSingleton.id) + XCTAssertNotEqual(sut1.isolatedToMainActorSingleton.id, sut3.isolatedToMainActorSingleton.id) + XCTAssertNotEqual(sut1.isolatedToCustomGlobalActorSingleton.id, sut3.isolatedToCustomGlobalActorSingleton.id) +} #endif