From 560dd68c6bc32a0321597aca329d7a363858c343 Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 22:05:30 -0600 Subject: [PATCH 1/2] test: Deepen unit tests for Observer and Strategy patterns --- .../Behavioral/ObserverTests.swift | 133 ++++++++++++++++-- .../Behavioral/StrategyTests.swift | 130 ++++++++--------- 2 files changed, 178 insertions(+), 85 deletions(-) diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/ObserverTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/ObserverTests.swift index b6b5073..93aa426 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/ObserverTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/ObserverTests.swift @@ -1,14 +1,10 @@ -// -// ObserverTests.swift -// DesignAlgorithmsKitTests -// -// Unit tests for Observer Pattern -// - import XCTest @testable import DesignAlgorithmsKit final class ObserverTests: XCTestCase { + + // MARK: - Basic Functionality + func testObserverPattern() { // Given class TestObservable: BaseObservable {} @@ -66,9 +62,7 @@ final class ObserverTests: XCTestCase { let id: String var receivedEvents: [Any] = [] - init(id: String) { - self.id = id - } + init(id: String) { self.id = id } func didReceiveNotification(from observable: any Observable, event: Any) { receivedEvents.append(event) @@ -88,5 +82,122 @@ final class ObserverTests: XCTestCase { XCTAssertEqual(observer1.receivedEvents.count, 1) XCTAssertEqual(observer2.receivedEvents.count, 1) } + + // MARK: - Weak Reference Tests + + func testWeakReference() { + class TestObservable: BaseObservable {} + class TestObserver: Observer { + func didReceiveNotification(from observable: any Observable, event: Any) {} + } + + let observable = TestObservable() + + // Create an observer in a local scope so it gets deallocated + var observer: TestObserver? = TestObserver() + weak var weakObserver = observer + + observable.addObserver(observer!) + XCTAssertNotNil(weakObserver) + + // Remove strong reference + observer = nil + + // Verify deallocation + XCTAssertNil(weakObserver, "Observer should have been deallocated") + + // Verify notification doesn't crash + observable.notifyObservers(event: "test") + + // Verify cleanup on next add + // The implementation cleans up on addObserver + observable.addObserver(TestObserver()) + // Cannot easily inspect internal array count without reflection or subclassing exposing it, + // but 'addObserver' is the trigger for cleanup in BaseObservable. + } + + // MARK: - Reentrancy and Concurrency + + func testReentrancySafe() { + // Test removing self during notification + class TestObservable: BaseObservable {} + class TestObserver: Observer { + var observable: TestObservable? + var receivedCount = 0 + + func didReceiveNotification(from observable: any Observable, event: Any) { + receivedCount += 1 + if let obs = self.observable { + obs.removeObserver(self) + } + } + } + + let observable = TestObservable() + let observer = TestObserver() + observer.observable = observable + + observable.addObserver(observer) + + // First notification triggers removal + observable.notifyObservers(event: "1") + XCTAssertEqual(observer.receivedCount, 1) + + // Second notification should not reach observer + observable.notifyObservers(event: "2") + XCTAssertEqual(observer.receivedCount, 1) + } + + func testConcurrentAccess() { + class TestObservable: BaseObservable {} + class TestObserver: Observer { + func didReceiveNotification(from observable: any Observable, event: Any) { + // Do work + } + } + + let observable = TestObservable() + let iterations = 1000 + let expectation = self.expectation(description: "Concurrent observer access") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + let observer = TestObserver() + observable.addObserver(observer) + + if i % 2 == 0 { + observable.notifyObservers(event: i) + } + + if i % 3 == 0 { + observable.removeObserver(observer) + } + + expectation.fulfill() + } + + waitForExpectations(timeout: 5.0) + } + + func testDuplicateAddition() { + class TestObservable: BaseObservable {} + class TestObserver: Observer { + var count = 0 + func didReceiveNotification(from observable: any Observable, event: Any) { + count += 1 + } + } + + let observable = TestObservable() + let observer = TestObserver() + + // Add twice + observable.addObserver(observer) + observable.addObserver(observer) + + observable.notifyObservers(event: "test") + + // Should only be notified once + XCTAssertEqual(observer.count, 1) + } } - diff --git a/Tests/DesignAlgorithmsKitTests/Behavioral/StrategyTests.swift b/Tests/DesignAlgorithmsKitTests/Behavioral/StrategyTests.swift index 1cb6c90..33157ac 100644 --- a/Tests/DesignAlgorithmsKitTests/Behavioral/StrategyTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Behavioral/StrategyTests.swift @@ -1,30 +1,20 @@ -// -// StrategyTests.swift -// DesignAlgorithmsKitTests -// -// Unit tests for Strategy Pattern -// - import XCTest @testable import DesignAlgorithmsKit final class StrategyTests: XCTestCase { + + // MARK: - Basic Implementation + func testStrategyPattern() { // Given struct AdditionStrategy: Strategy { let strategyID = "addition" - - func execute(_ a: Int, _ b: Int) -> Int { - return a + b - } + func execute(_ a: Int, _ b: Int) -> Int { return a + b } } struct MultiplicationStrategy: Strategy { let strategyID = "multiplication" - - func execute(_ a: Int, _ b: Int) -> Int { - return a * b - } + func execute(_ a: Int, _ b: Int) -> Int { return a * b } } // When @@ -39,92 +29,84 @@ final class StrategyTests: XCTestCase { XCTAssertEqual(result2, 15) } - func testStrategyContext() { - // Given - struct TestStrategy: Strategy { - let strategyID = "test" - } - - // When - let context = StrategyContext(strategy: TestStrategy()) - - // Then - XCTAssertEqual(context.getStrategy().strategyID, "test") - } - - func testStrategyContextSetStrategy() { + func testContextSwitching() { // Given struct TestStrategy: Strategy { let strategyID: String + let val: Int } - // When - let context = StrategyContext(strategy: TestStrategy(strategyID: "strategy1")) - XCTAssertEqual(context.getStrategy().strategyID, "strategy1") + let s1 = TestStrategy(strategyID: "s1", val: 1) + let s2 = TestStrategy(strategyID: "s2", val: 2) - context.setStrategy(TestStrategy(strategyID: "strategy2")) + let context = StrategyContext(strategy: s1) + XCTAssertEqual(context.getStrategy().val, 1) - // Then - XCTAssertEqual(context.getStrategy().strategyID, "strategy2") + context.setStrategy(s2) + XCTAssertEqual(context.getStrategy().val, 2) + + context.setStrategy(s1) + XCTAssertEqual(context.getStrategy().val, 1) } + // MARK: - Inheritance and Types + func testBaseStrategy() { - // Given let baseStrategy = BaseStrategy(strategyID: "base-test") - - // Then XCTAssertEqual(baseStrategy.strategyID, "base-test") } func testBaseStrategyInheritance() { - // Given class CustomStrategy: BaseStrategy { - init() { - super.init(strategyID: "custom") - } + init() { super.init(strategyID: "custom") } + func op() -> String { return "op" } } - // When let strategy = CustomStrategy() - - // Then XCTAssertEqual(strategy.strategyID, "custom") + XCTAssertEqual(strategy.op(), "op") } - func testStrategyContextGetStrategy() { - // Given - struct TestStrategy: Strategy { - let strategyID = "test" - } - - let context = StrategyContext(strategy: TestStrategy()) + // MARK: - Polymorphism + + protocol MathStrategy: Strategy { + func calculate(_ a: Int, _ b: Int) -> Int + } + + struct Add: MathStrategy { + var strategyID = "add" + func calculate(_ a: Int, _ b: Int) -> Int { a + b } + } + + struct Subtract: MathStrategy { + var strategyID = "subtract" + func calculate(_ a: Int, _ b: Int) -> Int { a - b } + } + + func testPolymorphicContext() { + let context = StrategyContext(strategy: BoxedMathStrategy(Add())) - // When - let retrievedStrategy = context.getStrategy() + XCTAssertEqual(context.getStrategy().calculate(10, 5), 15) - // Then - XCTAssertEqual(retrievedStrategy.strategyID, "test") + context.setStrategy(BoxedMathStrategy(Subtract())) + XCTAssertEqual(context.getStrategy().calculate(10, 5), 5) } - func testStrategyContextMultipleStrategies() { - // Given - struct TestStrategy: Strategy { - let strategyID: String + // Type erasure wrapper for the test because StrategyContext is generic over a specific concrete type + // or a protocol if used as an existentials (but StrategyContext

requires P: Strategy). + // Protocols conforming to protocols don't satisfy P: Strategy for generic constraints in Swift + // without `any` or box, testing here verifies the usage pattern. + struct BoxedMathStrategy: MathStrategy { + let strategyID: String + private let _calculate: (Int, Int) -> Int + + init(_ strategy: S) { + self.strategyID = strategy.strategyID + self._calculate = strategy.calculate } - // When - let context = StrategyContext(strategy: TestStrategy(strategyID: "strategy1")) - context.setStrategy(TestStrategy(strategyID: "strategy2")) - - // Then - XCTAssertEqual(context.getStrategy().strategyID, "strategy2") - } -} - -// Extension to make strategies executable for testing -extension Strategy { - func execute(_ a: Int, _ b: Int) -> Int { - return 0 // Default implementation + func calculate(_ a: Int, _ b: Int) -> Int { + _calculate(a, b) + } } } - From 4727b6ca2dbd88701f91b382b0884414af1bee6d Mon Sep 17 00:00:00 2001 From: Rick Hohler Date: Fri, 12 Dec 2025 22:22:33 -0600 Subject: [PATCH 2/2] test: Enhance Builder, Factory, Adapter, and Facade unit tests --- .../Creational/BuilderTests.swift | 385 ++++++------------ .../Creational/FactoryTests.swift | 111 ++--- .../Structural/AdapterTests.swift | 163 +++----- .../Structural/FacadeTests.swift | 223 +++------- 4 files changed, 255 insertions(+), 627 deletions(-) diff --git a/Tests/DesignAlgorithmsKitTests/Creational/BuilderTests.swift b/Tests/DesignAlgorithmsKitTests/Creational/BuilderTests.swift index abda4dd..432fe50 100644 --- a/Tests/DesignAlgorithmsKitTests/Creational/BuilderTests.swift +++ b/Tests/DesignAlgorithmsKitTests/Creational/BuilderTests.swift @@ -1,14 +1,10 @@ -// -// BuilderTests.swift -// DesignAlgorithmsKitTests -// -// Unit tests for Builder Pattern -// - import XCTest @testable import DesignAlgorithmsKit final class BuilderTests: XCTestCase { + + // MARK: - Basic Builder Pattern + func testBuilderPattern() throws { // Given struct TestObject { @@ -52,12 +48,11 @@ final class BuilderTests: XCTestCase { XCTAssertEqual(object.property2, 42) } + // MARK: - Error Handling + func testBuilderMissingProperty() { // Given - struct TestObject { - let property: String - } - + struct TestObject { let property: String } class TestObjectBuilder: BaseBuilder { override func build() throws -> TestObject { throw BuilderError.missingRequiredProperty("property") @@ -76,9 +71,7 @@ final class BuilderTests: XCTestCase { func testValidatingBuilder() throws { // Given - struct TestObject { - let value: Int - } + struct TestObject { let value: Int } class ValidatingBuilder: BaseBuilder, ValidatingBuilderProtocol { private var value: Int? @@ -89,12 +82,8 @@ final class BuilderTests: XCTestCase { } func validate() throws { - guard let value = value else { - throw BuilderError.missingRequiredProperty("value") - } - if value < 0 { - throw BuilderError.invalidValue("value", "must be non-negative") - } + guard let value = value else { throw BuilderError.missingRequiredProperty("value") } + if value < 0 { throw BuilderError.invalidValue("value", "must be non-negative") } } override func build() throws -> TestObject { @@ -103,30 +92,21 @@ final class BuilderTests: XCTestCase { } } - // When/Then - Valid value - let object = try ValidatingBuilder() - .setValue(42) - .build() + // When/Then - Valid + let object = try ValidatingBuilder().setValue(42).build() XCTAssertEqual(object.value, 42) - // When/Then - Invalid value + // When/Then - Invalid XCTAssertThrowsError(try ValidatingBuilder().setValue(-1).build()) } func testBaseBuilderNotImplemented() { - // Given - struct TestObject { - let value: String - } + struct TestObject { let value: String } + class TestBuilder: BaseBuilder {} - class TestBuilder: BaseBuilder { - // Doesn't override build() - } - - // When/Then XCTAssertThrowsError(try TestBuilder().build()) { error in if case BuilderError.notImplemented = error { - // Expected error + // Expected } else { XCTFail("Expected BuilderError.notImplemented, got \(error)") } @@ -134,21 +114,32 @@ final class BuilderTests: XCTestCase { } func testBuilderErrorLocalizedDescription() { - // Test notImplemented let notImplemented = BuilderError.notImplemented XCTAssertEqual(notImplemented.localizedDescription, "Builder build() method not implemented") - // Test missingRequiredProperty let missing = BuilderError.missingRequiredProperty("testProperty") XCTAssertEqual(missing.localizedDescription, "Required property 'testProperty' is missing") - // Test invalidValue let invalid = BuilderError.invalidValue("testProperty", "must be positive") XCTAssertEqual(invalid.localizedDescription, "Invalid value for 'testProperty': must be positive") } + func testBuilderErrorEquality() { + // Test that error cases can be matched + let error1 = BuilderError.missingRequiredProperty("test") + let error2 = BuilderError.missingRequiredProperty("test") + + if case BuilderError.missingRequiredProperty(let prop1) = error1, + case BuilderError.missingRequiredProperty(let prop2) = error2 { + XCTAssertEqual(prop1, prop2) + } else { + XCTFail("Error pattern matching failed") + } + } + + // MARK: - Advanced Usage + func testBuilderFluentAPI() throws { - // Given struct ComplexObject { let name: String let age: Int @@ -162,38 +153,19 @@ final class BuilderTests: XCTestCase { private var email: String? private var tags: [String] = [] - func setName(_ name: String) -> Self { - self.name = name - return self - } - - func setAge(_ age: Int) -> Self { - self.age = age - return self - } - - func setEmail(_ email: String?) -> Self { - self.email = email - return self - } - - func addTag(_ tag: String) -> Self { - self.tags.append(tag) - return self - } + func setName(_ name: String) -> Self { self.name = name; return self } + func setAge(_ age: Int) -> Self { self.age = age; return self } + func setEmail(_ email: String?) -> Self { self.email = email; return self } + func addTag(_ tag: String) -> Self { self.tags.append(tag); return self } override func build() throws -> ComplexObject { - guard let name = name else { - throw BuilderError.missingRequiredProperty("name") - } - guard let age = age else { - throw BuilderError.missingRequiredProperty("age") - } + guard let name = name else { throw BuilderError.missingRequiredProperty("name") } + guard let age = age else { throw BuilderError.missingRequiredProperty("age") } return ComplexObject(name: name, age: age, email: email, tags: tags) } } - // When - Test fluent API chaining + // Use fluent API let object = try ComplexObjectBuilder() .setName("John Doe") .setAge(30) @@ -202,15 +174,27 @@ final class BuilderTests: XCTestCase { .addTag("swift") .build() - // Then XCTAssertEqual(object.name, "John Doe") XCTAssertEqual(object.age, 30) XCTAssertEqual(object.email, "john@example.com") XCTAssertEqual(object.tags, ["developer", "swift"]) + + // Partial configuration test + // Verify order independence and state retention + let builder = ComplexObjectBuilder() + builder.setName("Jane").setAge(25) + + let object1 = try builder.build() + XCTAssertEqual(object1.name, "Jane") + + // Modify state + builder.setName("Jane Doe") + let object2 = try builder.build() + XCTAssertEqual(object2.name, "Jane Doe") + XCTAssertEqual(object2.age, 25) // Age preserved } func testBuilderWithOptionalProperties() throws { - // Given struct OptionalObject { let required: String let optional: String? @@ -220,175 +204,91 @@ final class BuilderTests: XCTestCase { private var required: String? private var optional: String? - func setRequired(_ value: String) -> Self { - self.required = value - return self - } - - func setOptional(_ value: String?) -> Self { - self.optional = value - return self - } + func setRequired(_ value: String) -> Self { self.required = value; return self } + func setOptional(_ value: String?) -> Self { self.optional = value; return self } override func build() throws -> OptionalObject { - guard let required = required else { - throw BuilderError.missingRequiredProperty("required") - } + guard let required = required else { throw BuilderError.missingRequiredProperty("required") } return OptionalObject(required: required, optional: optional) } } - // When/Then - With optional value - let object1 = try OptionalObjectBuilder() - .setRequired("required") - .setOptional("optional") - .build() - XCTAssertEqual(object1.required, "required") - XCTAssertEqual(object1.optional, "optional") + let object1 = try OptionalObjectBuilder().setRequired("req").setOptional("opt").build() + XCTAssertEqual(object1.optional, "opt") - // When/Then - Without optional value - let object2 = try OptionalObjectBuilder() - .setRequired("required") - .setOptional(nil) - .build() - XCTAssertEqual(object2.required, "required") + let object2 = try OptionalObjectBuilder().setRequired("req").setOptional(nil).build() XCTAssertNil(object2.optional) } func testBuilderMultipleInstances() throws { - // Given - struct SimpleObject { - let value: String - } - + struct SimpleObject { let value: String } class SimpleBuilder: BaseBuilder { private var value: String? - - func setValue(_ value: String) -> Self { - self.value = value - return self - } - + func setValue(_ value: String) -> Self { self.value = value; return self } override func build() throws -> SimpleObject { - guard let value = value else { - throw BuilderError.missingRequiredProperty("value") - } + guard let value = value else { throw BuilderError.missingRequiredProperty("value") } return SimpleObject(value: value) } } - // When - Create multiple instances let builder1 = SimpleBuilder() let builder2 = SimpleBuilder() let object1 = try builder1.setValue("value1").build() let object2 = try builder2.setValue("value2").build() - // Then - Each builder should be independent XCTAssertEqual(object1.value, "value1") XCTAssertEqual(object2.value, "value2") } func testValidatingBuilderProtocolDefault() throws { - // Given - struct TestObject { - let value: Int - } - + struct TestObject { let value: Int } class DefaultValidatingBuilder: BaseBuilder, ValidatingBuilderProtocol { private var value: Int? - - func setValue(_ value: Int) -> Self { - self.value = value - return self - } - + func setValue(_ value: Int) -> Self { self.value = value; return self } override func build() throws -> TestObject { - // Explicitly call validate() to ensure default implementation is covered - try validate() - guard let value = value else { - throw BuilderError.missingRequiredProperty("value") - } + try validate() // Default does nothing + guard let value = value else { throw BuilderError.missingRequiredProperty("value") } return TestObject(value: value) } } - // When/Then - Default validation should not throw - let object = try DefaultValidatingBuilder() - .setValue(42) - .build() + let object = try DefaultValidatingBuilder().setValue(42).build() XCTAssertEqual(object.value, 42) } func testValidatingBuilderProtocolDefaultImplementation() throws { - // Given - Test the default validate() implementation directly - struct TestObject { - let value: String - } - + // Direct test of default implementation + struct TestObject { let value: String } class DirectValidateBuilder: BaseBuilder, ValidatingBuilderProtocol { private var value: String? - - func setValue(_ value: String) -> Self { - self.value = value - return self - } - + func setValue(_ value: String) -> Self { self.value = value; return self } override func build() throws -> TestObject { - // Call validate() explicitly to test default implementation - try validate() // Default implementation does nothing - guard let value = value else { - throw BuilderError.missingRequiredProperty("value") - } + try validate() + guard let value = value else { throw BuilderError.missingRequiredProperty("value") } return TestObject(value: value) } } - // When/Then - Default validate() should not throw - let object = try DirectValidateBuilder() - .setValue("test") - .build() + let object = try DirectValidateBuilder().setValue("test").build() XCTAssertEqual(object.value, "test") } func testValidatingBuilderWithCustomValidation() throws { - // Given - struct User { - let username: String - let age: Int - } + struct User { let username: String; let age: Int } class UserBuilder: BaseBuilder, ValidatingBuilderProtocol { private var username: String? private var age: Int? - func setUsername(_ username: String) -> Self { - self.username = username - return self - } - - func setAge(_ age: Int) -> Self { - self.age = age - return self - } + func setUsername(_ username: String) -> Self { self.username = username; return self } + func setAge(_ age: Int) -> Self { self.age = age; return self } func validate() throws { - guard let username = username else { - throw BuilderError.missingRequiredProperty("username") - } - if username.count < 3 { - throw BuilderError.invalidValue("username", "must be at least 3 characters") - } - - guard let age = age else { - throw BuilderError.missingRequiredProperty("age") - } - if age < 0 { - throw BuilderError.invalidValue("age", "must be non-negative") - } - if age > 150 { - throw BuilderError.invalidValue("age", "must be less than 150") - } + guard let username = username else { throw BuilderError.missingRequiredProperty("username") } + if username.count < 3 { throw BuilderError.invalidValue("username", "curr too short") } + guard let age = age else { throw BuilderError.missingRequiredProperty("age") } + if age < 0 || age > 150 { throw BuilderError.invalidValue("age", "invalid age") } } override func build() throws -> User { @@ -397,107 +297,64 @@ final class BuilderTests: XCTestCase { } } - // When/Then - Valid user - let validUser = try UserBuilder() - .setUsername("johndoe") - .setAge(30) - .build() - XCTAssertEqual(validUser.username, "johndoe") - XCTAssertEqual(validUser.age, 30) + // Happy path + let user = try UserBuilder().setUsername("john").setAge(30).build() + XCTAssertEqual(user.username, "john") - // When/Then - Invalid username (too short) - XCTAssertThrowsError(try UserBuilder().setUsername("ab").setAge(30).build()) { error in - if case BuilderError.invalidValue(let property, _) = error { - XCTAssertEqual(property, "username") - } else { - XCTFail("Expected invalidValue error for username") - } - } - - // When/Then - Invalid age (negative) - XCTAssertThrowsError(try UserBuilder().setUsername("johndoe").setAge(-1).build()) { error in - if case BuilderError.invalidValue(let property, _) = error { - XCTAssertEqual(property, "age") - } else { - XCTFail("Expected invalidValue error for age") + // Invalid checks + XCTAssertThrowsError(try UserBuilder().setUsername("a").setAge(30).build()) + XCTAssertThrowsError(try UserBuilder().setUsername("john").setAge(-1).build()) + } + + func testBuilderReuse() throws { + struct Config { let host: String; let port: Int } + class ConfigBuilder: BaseBuilder { + private var host: String? + private var port: Int? + func setHost(_ host: String) -> Self { self.host = host; return self } + func setPort(_ port: Int) -> Self { self.port = port; return self } + override func build() throws -> Config { + guard let h = host, let p = port else { throw BuilderError.missingRequiredProperty("config") } + return Config(host: h, port: p) } } - // When/Then - Invalid age (too high) - XCTAssertThrowsError(try UserBuilder().setUsername("johndoe").setAge(200).build()) { error in - if case BuilderError.invalidValue(let property, _) = error { - XCTAssertEqual(property, "age") - } else { - XCTFail("Expected invalidValue error for age") - } - } - } - - func testBuilderErrorEquality() { - // Test that error cases can be matched - let error1 = BuilderError.missingRequiredProperty("test") - let error2 = BuilderError.missingRequiredProperty("test") + let builder = ConfigBuilder() + let c1 = try builder.setHost("h1").setPort(1).build() + XCTAssertEqual(c1.host, "h1") - // Use pattern matching to verify - if case BuilderError.missingRequiredProperty(let prop1) = error1, - case BuilderError.missingRequiredProperty(let prop2) = error2 { - XCTAssertEqual(prop1, prop2) - } else { - XCTFail("Error pattern matching failed") - } + // Reuse + let c2 = try builder.setPort(2).build() + XCTAssertEqual(c2.host, "h1") // Host persisted + XCTAssertEqual(c2.port, 2) } - func testBuilderReuse() throws { + func testChainedOrderIndependence() throws { // Given - struct Config { - let host: String - let port: Int + struct Style { + let color: String + let size: Int } - class ConfigBuilder: BaseBuilder { - private var host: String? - private var port: Int? + class StyleBuilder: BaseBuilder