Skip to content

Commit 789a952

Browse files
committed
test: Add comprehensive unit tests for Builder pattern
- Added test for BaseBuilder default implementation (notImplemented error) - Added tests for BuilderError localizedDescription for all error cases - Added test for fluent API chaining with multiple method calls - Added test for optional properties handling - Added test for multiple independent builder instances - Added test for ValidatingBuilderProtocol default implementation - Added comprehensive validation tests with multiple rules - Added test for error pattern matching - Added test for builder reuse scenarios Total: 12 tests (up from 3), all passing
1 parent b72ccdb commit 789a952

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

Tests/DesignAlgorithmsKitTests/Creational/BuilderTests.swift

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,359 @@ final class BuilderTests: XCTestCase {
112112
// When/Then - Invalid value
113113
XCTAssertThrowsError(try ValidatingBuilder().setValue(-1).build())
114114
}
115+
116+
func testBaseBuilderNotImplemented() {
117+
// Given
118+
struct TestObject {
119+
let value: String
120+
}
121+
122+
class TestBuilder: BaseBuilder<TestObject> {
123+
// Doesn't override build()
124+
}
125+
126+
// When/Then
127+
XCTAssertThrowsError(try TestBuilder().build()) { error in
128+
if case BuilderError.notImplemented = error {
129+
// Expected error
130+
} else {
131+
XCTFail("Expected BuilderError.notImplemented, got \(error)")
132+
}
133+
}
134+
}
135+
136+
func testBuilderErrorLocalizedDescription() {
137+
// Test notImplemented
138+
let notImplemented = BuilderError.notImplemented
139+
XCTAssertEqual(notImplemented.localizedDescription, "Builder build() method not implemented")
140+
141+
// Test missingRequiredProperty
142+
let missing = BuilderError.missingRequiredProperty("testProperty")
143+
XCTAssertEqual(missing.localizedDescription, "Required property 'testProperty' is missing")
144+
145+
// Test invalidValue
146+
let invalid = BuilderError.invalidValue("testProperty", "must be positive")
147+
XCTAssertEqual(invalid.localizedDescription, "Invalid value for 'testProperty': must be positive")
148+
}
149+
150+
func testBuilderFluentAPI() throws {
151+
// Given
152+
struct ComplexObject {
153+
let name: String
154+
let age: Int
155+
let email: String?
156+
let tags: [String]
157+
}
158+
159+
class ComplexObjectBuilder: BaseBuilder<ComplexObject> {
160+
private var name: String?
161+
private var age: Int?
162+
private var email: String?
163+
private var tags: [String] = []
164+
165+
func setName(_ name: String) -> Self {
166+
self.name = name
167+
return self
168+
}
169+
170+
func setAge(_ age: Int) -> Self {
171+
self.age = age
172+
return self
173+
}
174+
175+
func setEmail(_ email: String?) -> Self {
176+
self.email = email
177+
return self
178+
}
179+
180+
func addTag(_ tag: String) -> Self {
181+
self.tags.append(tag)
182+
return self
183+
}
184+
185+
override func build() throws -> ComplexObject {
186+
guard let name = name else {
187+
throw BuilderError.missingRequiredProperty("name")
188+
}
189+
guard let age = age else {
190+
throw BuilderError.missingRequiredProperty("age")
191+
}
192+
return ComplexObject(name: name, age: age, email: email, tags: tags)
193+
}
194+
}
195+
196+
// When - Test fluent API chaining
197+
let object = try ComplexObjectBuilder()
198+
.setName("John Doe")
199+
.setAge(30)
200+
.setEmail("john@example.com")
201+
.addTag("developer")
202+
.addTag("swift")
203+
.build()
204+
205+
// Then
206+
XCTAssertEqual(object.name, "John Doe")
207+
XCTAssertEqual(object.age, 30)
208+
XCTAssertEqual(object.email, "john@example.com")
209+
XCTAssertEqual(object.tags, ["developer", "swift"])
210+
}
211+
212+
func testBuilderWithOptionalProperties() throws {
213+
// Given
214+
struct OptionalObject {
215+
let required: String
216+
let optional: String?
217+
}
218+
219+
class OptionalObjectBuilder: BaseBuilder<OptionalObject> {
220+
private var required: String?
221+
private var optional: String?
222+
223+
func setRequired(_ value: String) -> Self {
224+
self.required = value
225+
return self
226+
}
227+
228+
func setOptional(_ value: String?) -> Self {
229+
self.optional = value
230+
return self
231+
}
232+
233+
override func build() throws -> OptionalObject {
234+
guard let required = required else {
235+
throw BuilderError.missingRequiredProperty("required")
236+
}
237+
return OptionalObject(required: required, optional: optional)
238+
}
239+
}
240+
241+
// When/Then - With optional value
242+
let object1 = try OptionalObjectBuilder()
243+
.setRequired("required")
244+
.setOptional("optional")
245+
.build()
246+
XCTAssertEqual(object1.required, "required")
247+
XCTAssertEqual(object1.optional, "optional")
248+
249+
// When/Then - Without optional value
250+
let object2 = try OptionalObjectBuilder()
251+
.setRequired("required")
252+
.setOptional(nil)
253+
.build()
254+
XCTAssertEqual(object2.required, "required")
255+
XCTAssertNil(object2.optional)
256+
}
257+
258+
func testBuilderMultipleInstances() throws {
259+
// Given
260+
struct SimpleObject {
261+
let value: String
262+
}
263+
264+
class SimpleBuilder: BaseBuilder<SimpleObject> {
265+
private var value: String?
266+
267+
func setValue(_ value: String) -> Self {
268+
self.value = value
269+
return self
270+
}
271+
272+
override func build() throws -> SimpleObject {
273+
guard let value = value else {
274+
throw BuilderError.missingRequiredProperty("value")
275+
}
276+
return SimpleObject(value: value)
277+
}
278+
}
279+
280+
// When - Create multiple instances
281+
let builder1 = SimpleBuilder()
282+
let builder2 = SimpleBuilder()
283+
284+
let object1 = try builder1.setValue("value1").build()
285+
let object2 = try builder2.setValue("value2").build()
286+
287+
// Then - Each builder should be independent
288+
XCTAssertEqual(object1.value, "value1")
289+
XCTAssertEqual(object2.value, "value2")
290+
}
291+
292+
func testValidatingBuilderProtocolDefault() throws {
293+
// Given
294+
struct TestObject {
295+
let value: Int
296+
}
297+
298+
class DefaultValidatingBuilder: BaseBuilder<TestObject>, ValidatingBuilderProtocol {
299+
private var value: Int?
300+
301+
func setValue(_ value: Int) -> Self {
302+
self.value = value
303+
return self
304+
}
305+
306+
override func build() throws -> TestObject {
307+
guard let value = value else {
308+
throw BuilderError.missingRequiredProperty("value")
309+
}
310+
return TestObject(value: value)
311+
}
312+
}
313+
314+
// When/Then - Default validation should not throw
315+
let object = try DefaultValidatingBuilder()
316+
.setValue(42)
317+
.build()
318+
XCTAssertEqual(object.value, 42)
319+
}
320+
321+
func testValidatingBuilderWithCustomValidation() throws {
322+
// Given
323+
struct User {
324+
let username: String
325+
let age: Int
326+
}
327+
328+
class UserBuilder: BaseBuilder<User>, ValidatingBuilderProtocol {
329+
private var username: String?
330+
private var age: Int?
331+
332+
func setUsername(_ username: String) -> Self {
333+
self.username = username
334+
return self
335+
}
336+
337+
func setAge(_ age: Int) -> Self {
338+
self.age = age
339+
return self
340+
}
341+
342+
func validate() throws {
343+
guard let username = username else {
344+
throw BuilderError.missingRequiredProperty("username")
345+
}
346+
if username.count < 3 {
347+
throw BuilderError.invalidValue("username", "must be at least 3 characters")
348+
}
349+
350+
guard let age = age else {
351+
throw BuilderError.missingRequiredProperty("age")
352+
}
353+
if age < 0 {
354+
throw BuilderError.invalidValue("age", "must be non-negative")
355+
}
356+
if age > 150 {
357+
throw BuilderError.invalidValue("age", "must be less than 150")
358+
}
359+
}
360+
361+
override func build() throws -> User {
362+
try validate()
363+
return User(username: username!, age: age!)
364+
}
365+
}
366+
367+
// When/Then - Valid user
368+
let validUser = try UserBuilder()
369+
.setUsername("johndoe")
370+
.setAge(30)
371+
.build()
372+
XCTAssertEqual(validUser.username, "johndoe")
373+
XCTAssertEqual(validUser.age, 30)
374+
375+
// When/Then - Invalid username (too short)
376+
XCTAssertThrowsError(try UserBuilder().setUsername("ab").setAge(30).build()) { error in
377+
if case BuilderError.invalidValue(let property, _) = error {
378+
XCTAssertEqual(property, "username")
379+
} else {
380+
XCTFail("Expected invalidValue error for username")
381+
}
382+
}
383+
384+
// When/Then - Invalid age (negative)
385+
XCTAssertThrowsError(try UserBuilder().setUsername("johndoe").setAge(-1).build()) { error in
386+
if case BuilderError.invalidValue(let property, _) = error {
387+
XCTAssertEqual(property, "age")
388+
} else {
389+
XCTFail("Expected invalidValue error for age")
390+
}
391+
}
392+
393+
// When/Then - Invalid age (too high)
394+
XCTAssertThrowsError(try UserBuilder().setUsername("johndoe").setAge(200).build()) { error in
395+
if case BuilderError.invalidValue(let property, _) = error {
396+
XCTAssertEqual(property, "age")
397+
} else {
398+
XCTFail("Expected invalidValue error for age")
399+
}
400+
}
401+
}
402+
403+
func testBuilderErrorEquality() {
404+
// Test that error cases can be matched
405+
let error1 = BuilderError.missingRequiredProperty("test")
406+
let error2 = BuilderError.missingRequiredProperty("test")
407+
408+
// Use pattern matching to verify
409+
if case BuilderError.missingRequiredProperty(let prop1) = error1,
410+
case BuilderError.missingRequiredProperty(let prop2) = error2 {
411+
XCTAssertEqual(prop1, prop2)
412+
} else {
413+
XCTFail("Error pattern matching failed")
414+
}
415+
}
416+
417+
func testBuilderReuse() throws {
418+
// Given
419+
struct Config {
420+
let host: String
421+
let port: Int
422+
}
423+
424+
class ConfigBuilder: BaseBuilder<Config> {
425+
private var host: String?
426+
private var port: Int?
427+
428+
func setHost(_ host: String) -> Self {
429+
self.host = host
430+
return self
431+
}
432+
433+
func setPort(_ port: Int) -> Self {
434+
self.port = port
435+
return self
436+
}
437+
438+
override func build() throws -> Config {
439+
guard let host = host else {
440+
throw BuilderError.missingRequiredProperty("host")
441+
}
442+
guard let port = port else {
443+
throw BuilderError.missingRequiredProperty("port")
444+
}
445+
return Config(host: host, port: port)
446+
}
447+
}
448+
449+
// When - Reuse builder instance
450+
let builder = ConfigBuilder()
451+
452+
let config1 = try builder
453+
.setHost("localhost")
454+
.setPort(8080)
455+
.build()
456+
457+
// Reset and build again
458+
let config2 = try builder
459+
.setHost("example.com")
460+
.setPort(443)
461+
.build()
462+
463+
// Then - Last values should be used
464+
XCTAssertEqual(config1.host, "localhost")
465+
XCTAssertEqual(config1.port, 8080)
466+
XCTAssertEqual(config2.host, "example.com")
467+
XCTAssertEqual(config2.port, 443)
468+
}
115469
}
116470

0 commit comments

Comments
 (0)