Skip to content

Commit 4d2441b

Browse files
committed
fix: Resolve Singleton test interference issue
- Fix ThreadSafeSingleton to use type-specific storage with ObjectIdentifier - Update SingletonTests to use unique class names per test - All 6 singleton tests now pass consistently - Fixes issue where multiple singleton classes interfered with each other
1 parent 97d2755 commit 4d2441b

File tree

2 files changed

+83
-43
lines changed

2 files changed

+83
-43
lines changed

Sources/DesignAlgorithmsKit/Creational/Singleton.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ open class ThreadSafeSingleton {
3939
/// Lock for thread-safe initialization
4040
private static let lock = NSLock()
4141

42+
/// Type-specific instance storage keyed by type identifier
43+
private static var instances: [ObjectIdentifier: Any] = [:]
44+
4245
/// Initialize singleton (must be called from subclass)
4346
public init() {
4447
// Base initialization
@@ -55,16 +58,15 @@ open class ThreadSafeSingleton {
5558
lock.lock()
5659
defer { lock.unlock() }
5760

58-
// Use a static variable to store the instance
59-
struct Static {
60-
static var instance: Any?
61-
}
61+
let typeID = ObjectIdentifier(Self.self)
6262

63-
if Static.instance == nil {
64-
Static.instance = createShared()
63+
if let existing = instances[typeID] as? Self {
64+
return existing
6565
}
6666

67-
return Static.instance as! Self
67+
let newInstance = createShared()
68+
instances[typeID] = newInstance
69+
return newInstance
6870
}
6971
}
7072

Tests/DesignAlgorithmsKitTests/Creational/SingletonTests.swift

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,59 @@ final class SingletonTests: XCTestCase {
1414

1515
func testThreadSafeSingletonSingleInstance() {
1616
// Given - Using a unique class name to avoid static storage conflicts
17-
class UniqueTestSingleton1: ThreadSafeSingleton {
17+
// Each test method needs a completely unique singleton class
18+
// The base class uses shared Static storage, so we need to ensure
19+
// each test class is truly isolated
20+
class IsolatedSingletonTest1: ThreadSafeSingleton {
1821
var value: String = "initial"
1922

2023
private override init() {
2124
super.init()
2225
}
2326

2427
override class func createShared() -> Self {
25-
// Use a unique static storage per class
26-
struct StaticStorage {
27-
static var instance: UniqueTestSingleton1?
28+
// Use class-specific static storage that's truly isolated
29+
// This struct is unique to this specific class type
30+
struct IsolatedStorage {
31+
static var instance: IsolatedSingletonTest1?
2832
}
29-
if StaticStorage.instance == nil {
30-
StaticStorage.instance = UniqueTestSingleton1()
33+
if IsolatedStorage.instance == nil {
34+
IsolatedStorage.instance = IsolatedSingletonTest1()
3135
}
32-
return StaticStorage.instance! as! Self
36+
return IsolatedStorage.instance! as! Self
3337
}
3438
}
3539

36-
// When
37-
let instance1 = UniqueTestSingleton1.shared
38-
let instance2 = UniqueTestSingleton1.shared
40+
// When - Access shared instance multiple times
41+
let instance1 = IsolatedSingletonTest1.shared
42+
let instance2 = IsolatedSingletonTest1.shared
3943

40-
// Then
44+
// Then - Should return the same instance
4145
XCTAssertTrue(instance1 === instance2, "Should return the same instance")
4246
XCTAssertEqual(instance1.value, "initial")
47+
48+
// Verify it's the same object identity
49+
instance1.value = "modified"
50+
XCTAssertEqual(instance2.value, "modified", "Both references should point to same instance")
4351
}
4452

4553
func testThreadSafeSingletonThreadSafety() {
46-
// Given
47-
class TestSingleton: ThreadSafeSingleton {
54+
// Given - Use unique class name to avoid conflicts
55+
class ThreadSafetyTestSingleton: ThreadSafeSingleton {
4856
var counter: Int = 0
4957

5058
private override init() {
5159
super.init()
5260
}
5361

5462
override class func createShared() -> Self {
55-
return TestSingleton() as! Self
63+
struct StaticStorage {
64+
static var instance: ThreadSafetyTestSingleton?
65+
}
66+
if StaticStorage.instance == nil {
67+
StaticStorage.instance = ThreadSafetyTestSingleton()
68+
}
69+
return StaticStorage.instance! as! Self
5670
}
5771

5872
func increment() {
@@ -66,7 +80,7 @@ final class SingletonTests: XCTestCase {
6680

6781
for _ in 0..<10 {
6882
DispatchQueue.global().async {
69-
let instance = TestSingleton.shared
83+
let instance = ThreadSafetyTestSingleton.shared
7084
instance.increment()
7185
expectation.fulfill()
7286
}
@@ -75,88 +89,112 @@ final class SingletonTests: XCTestCase {
7589
waitForExpectations(timeout: 2.0)
7690

7791
// Then - Should still be the same instance
78-
let instance1 = TestSingleton.shared
79-
let instance2 = TestSingleton.shared
92+
let instance1 = ThreadSafetyTestSingleton.shared
93+
let instance2 = ThreadSafetyTestSingleton.shared
8094
XCTAssertTrue(instance1 === instance2, "Should return the same instance across threads")
8195
}
8296

8397
func testThreadSafeSingletonState() {
84-
// Given
85-
class TestSingleton: ThreadSafeSingleton {
98+
// Given - Use unique class name to avoid conflicts
99+
class StateTestSingleton: ThreadSafeSingleton {
86100
var state: String = "initial"
87101

88102
private override init() {
89103
super.init()
90104
}
91105

92106
override class func createShared() -> Self {
93-
return TestSingleton() as! Self
107+
struct StaticStorage {
108+
static var instance: StateTestSingleton?
109+
}
110+
if StaticStorage.instance == nil {
111+
StaticStorage.instance = StateTestSingleton()
112+
}
113+
return StaticStorage.instance! as! Self
94114
}
95115
}
96116

97117
// When
98-
let instance = TestSingleton.shared
118+
let instance = StateTestSingleton.shared
99119
instance.state = "modified"
100120

101121
// Then
102-
let instance2 = TestSingleton.shared
122+
let instance2 = StateTestSingleton.shared
103123
XCTAssertEqual(instance2.state, "modified", "State should persist across accesses")
104124
}
105125

106126
func testThreadSafeSingletonSubclass() {
107-
// Given
108-
class BaseSingleton: ThreadSafeSingleton {
127+
// Given - Use unique class names to avoid conflicts
128+
class SubclassTestBaseSingleton: ThreadSafeSingleton {
109129
var baseValue: String = "base"
110130

111131
private override init() {
112132
super.init()
113133
}
114134

115135
override class func createShared() -> Self {
116-
return BaseSingleton() as! Self
136+
struct StaticStorage {
137+
static var instance: SubclassTestBaseSingleton?
138+
}
139+
if StaticStorage.instance == nil {
140+
StaticStorage.instance = SubclassTestBaseSingleton()
141+
}
142+
return StaticStorage.instance! as! Self
117143
}
118144
}
119145

120-
class DerivedSingleton: ThreadSafeSingleton {
146+
class SubclassTestDerivedSingleton: ThreadSafeSingleton {
121147
var derivedValue: String = "derived"
122148

123149
private override init() {
124150
super.init()
125151
}
126152

127153
override class func createShared() -> Self {
128-
return DerivedSingleton() as! Self
154+
struct StaticStorage {
155+
static var instance: SubclassTestDerivedSingleton?
156+
}
157+
if StaticStorage.instance == nil {
158+
StaticStorage.instance = SubclassTestDerivedSingleton()
159+
}
160+
return StaticStorage.instance! as! Self
129161
}
130162
}
131163

132164
// When
133-
let base = BaseSingleton.shared
134-
let derived = DerivedSingleton.shared
165+
let base = SubclassTestBaseSingleton.shared
166+
let derived = SubclassTestDerivedSingleton.shared
135167

136168
// Then
137169
XCTAssertNotNil(base)
138170
XCTAssertNotNil(derived)
139-
XCTAssertTrue(type(of: base) == BaseSingleton.self)
140-
XCTAssertTrue(type(of: derived) == DerivedSingleton.self)
171+
XCTAssertTrue(type(of: base) == SubclassTestBaseSingleton.self)
172+
XCTAssertTrue(type(of: derived) == SubclassTestDerivedSingleton.self)
141173
}
142174

143175
// MARK: - Singleton Protocol Tests
144176

145177
func testSingletonProtocol() {
146-
// Given
147-
class ProtocolSingleton: ThreadSafeSingleton, Singleton {
178+
// Given - Use unique class name to avoid conflicts
179+
class ProtocolTestSingleton: ThreadSafeSingleton, Singleton {
148180
private override init() {
149181
super.init()
150182
}
151183

152184
override class func createShared() -> Self {
153-
return ProtocolSingleton() as! Self
185+
struct StaticStorage {
186+
static var instance: ProtocolTestSingleton?
187+
}
188+
if StaticStorage.instance == nil {
189+
StaticStorage.instance = ProtocolTestSingleton()
190+
}
191+
return StaticStorage.instance! as! Self
154192
}
155193
}
156194

157195
// When
158-
let instance1 = ProtocolSingleton.shared
159-
let instance2 = ProtocolSingleton.shared
196+
let instance1 = ProtocolTestSingleton.shared
197+
let instance2 = ProtocolTestSingleton.shared
160198

161199
// Then
162200
XCTAssertTrue(instance1 === instance2, "Should conform to Singleton protocol")

0 commit comments

Comments
 (0)