From 1b53f1a8ea5539f5180fe7f1034079b92861fdeb Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Sat, 23 Aug 2025 15:29:36 -0700 Subject: [PATCH] Refactor tests --- di_test.go | 649 ++++++++++++++++++++++++++++------------------------- 1 file changed, 344 insertions(+), 305 deletions(-) diff --git a/di_test.go b/di_test.go index 68e54fb7bacb749e36fedca12341cae6c8266579..005a293ee5df00a534cf7a7e2b5069b08b448904 100644 --- a/di_test.go +++ b/di_test.go @@ -74,346 +74,385 @@ func TestBuildSuccess(t *testing.T) { a *A cs []C } - - type Config struct { - MakeA Provide[*A] - MakeB Provide[*B] - MakeCs []Provide[C] - } - - cfg := Config{ - MakeA: MustProvide[*A](func() (*A, error) { - return &A{val: "hello"}, nil - }), - MakeB: MustProvide[*B](func(a *A, cs []C) *B { - return &B{a: a, cs: cs} - }), - MakeCs: []Provide[C]{ - MustProvide[C](C{val: 1}), - MustProvide[C](func() (C, error) { - return C{val: 2}, nil - })}, - } - - type Result struct { - A *A - B *B - } - var res Result - err := Build(cfg, &res) - if err != nil { - t.Fatalf("Build failed: %v", err) - } - if res.A == nil { - t.Fatalf("expected res.A to be populated") - } - if res.B == nil { - t.Fatalf("expected res.B to be populated") - } - if res.B.a != res.A { - t.Fatalf("expected B.a to reference A instance") - } - if len(res.B.cs) != 2 { - t.Fatalf("wrong count. Saw %d", len(res.B.cs)) - } - if res.B.cs[0].val != 1 { - t.Fatalf("wrong value") - } - if res.B.cs[1].val != 2 { - t.Fatalf("wrong value") - } - if res.A.val != "hello" { - t.Fatalf("unexpected A value: %s", res.A.val) - } -} - -func TestBuildSuccess2(t *testing.T) { - type A struct { - val string - } - type B struct { - a *A - } - - type Config struct { - MakeA func() (*A, error) - MakeB func(*A) (*B, error) - } - - type Result struct { - A *A + type NestedConfig struct { + OtherSetting bool + NestedDecision func(c NestedConfig) uint } - - cfg := Config{ - MakeA: func() (*A, error) { - return &A{val: "hello"}, nil + type ANum int + type ConfigWithInner struct { + NestedConfig + SomeSetting bool + Inner func(c ConfigWithInner) int + } + + tests := []struct { + name string + config interface{} + result interface{} + verify func(t *testing.T, result interface{}) + }{ + { + name: "complex dependencies with providers", + config: struct { + MakeA Provide[*A] + MakeB Provide[*B] + MakeCs []Provide[C] + }{ + MakeA: MustProvide[*A](func() (*A, error) { + return &A{val: "hello"}, nil + }), + MakeB: MustProvide[*B](func(a *A, cs []C) *B { + return &B{a: a, cs: cs} + }), + MakeCs: []Provide[C]{ + MustProvide[C](C{val: 1}), + MustProvide[C](func() (C, error) { + return C{val: 2}, nil + }), + }, + }, + result: &struct { + A *A + B *B + }{}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A *A + B *B + }) + if res.A == nil { + t.Fatalf("expected res.A to be populated") + } + if res.B == nil { + t.Fatalf("expected res.B to be populated") + } + if res.B.a != res.A { + t.Fatalf("expected B.a to reference A instance") + } + if len(res.B.cs) != 2 { + t.Fatalf("wrong count. Saw %d", len(res.B.cs)) + } + if res.B.cs[0].val != 1 { + t.Fatalf("wrong value") + } + if res.B.cs[1].val != 2 { + t.Fatalf("wrong value") + } + if res.A.val != "hello" { + t.Fatalf("unexpected A value: %s", res.A.val) + } + }, }, - MakeB: func(a *A) (*B, error) { - panic("Unexpected call to MakeB") - // (removed unreachable code after panic) + { + name: "simple function constructors", + config: struct { + MakeA func() (*A, error) + MakeB func(*A) (*B, error) + }{ + MakeA: func() (*A, error) { + return &A{val: "hello"}, nil + }, + MakeB: func(a *A) (*B, error) { + panic("Unexpected call to MakeB") + }, + }, + result: &struct { + A *A + }{}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A *A + }) + if res.A == nil { + t.Fatalf("expected res.A to be populated") + } + if res.A.val != "hello" { + t.Fatalf("unexpected A value: %s", res.A.val) + } + }, }, - } - - var res Result - err := Build(cfg, &res) - if err != nil { - t.Fatalf("Build failed: %v", err) - } - if res.A == nil { - t.Fatalf("expected res.A to be populated") - } - if res.A.val != "hello" { - t.Fatalf("unexpected A value: %s", res.A.val) - } -} - -// Test that constructor error is propagated. -func TestBuildConstructorError(t *testing.T) { - type A struct{} - sentinel := errors.New("boom") - - type Config struct { - MakeA func() (*A, error) - } - type Result struct { - A *A - } - - cfg := Config{ - MakeA: func() (*A, error) { - return nil, sentinel + { + name: "pre-supplied values", + config: struct { + A *A + MB func(*A) (*B, error) + }{ + A: &A{val: "pre-supplied"}, + MB: func(a *A) (*B, error) { + return &B{a: a}, nil + }, + }, + result: &struct { + A *A + B *B + }{}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A *A + B *B + }) + if res.A == nil || res.A.val != "pre-supplied" { + t.Fatalf("expected pre-supplied A, got %+v", res.A) + } + if res.B == nil || res.B.a != res.A { + t.Fatalf("expected B referencing A, got %+v", res.B) + } + }, + }, + { + name: "type aliases", + config: struct { + A ANum + B int + }{A: 3, B: 4}, + result: &struct { + A ANum + }{}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A ANum + }) + if res.A != 3 { + t.Fatalf("expected A=3, got %v", res.A) + } + }, + }, + { + name: "reference config in constructors", + config: ConfigWithInner{ + SomeSetting: true, + Inner: func(c ConfigWithInner) int { + if c.SomeSetting { + return 1 + } + return 0 + }, + NestedConfig: NestedConfig{ + OtherSetting: true, + NestedDecision: func(c NestedConfig) uint { + if c.OtherSetting { + return 1 + } + return 0 + }, + }, + }, + result: &struct { + A int + B uint + }{}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A int + B uint + }) + if res.A != 1 { + t.Fatalf("expected A=1, got %v", res.A) + } + if res.B != 1 { + t.Fatalf("expected B=1, got %v", res.B) + } + }, }, } - var res Result - err := Build(cfg, &res) - if err == nil { - t.Fatalf("expected error") - } - if !strings.Contains(err.Error(), "MakeA") { - t.Fatalf("expected error to mention constructor name, got: %v", err) - } - if !strings.Contains(err.Error(), "boom") { - t.Fatalf("expected original error message, got: %v", err) - } - if res.A != nil { - t.Fatalf("result A should not be populated on constructor failure") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Build(tt.config, tt.result) + if err != nil { + t.Fatalf("Build failed: %v", err) + } + tt.verify(t, tt.result) + }) } } -// Test missing dependency (constructor requires *A but *A not provided). -func TestBuildMissingDependency(t *testing.T) { +func TestBuildErrors(t *testing.T) { type A struct{} type B struct { a *A } - - type Config struct { - MakeB func(*A) (*B, error) - } - type Result struct { - B *B - } - - cfg := Config{ - MakeB: func(a *A) (*B, error) { - return &B{a: a}, nil - }, - } - var res Result - err := Build(cfg, &res) - if err == nil { - t.Fatalf("expected missing dependency error") - } - // Parameter type string should appear (may be *di.A). - if !strings.Contains(err.Error(), "*di.A") && !strings.Contains(err.Error(), "di.A") { - t.Fatalf("expected error to mention missing type *di.A, got: %v", err) - } - if res.B != nil { - t.Fatalf("result B should not be populated") - } -} - -// Test cycle detection between X and Y. -func TestBuildCycleDetection(t *testing.T) { type X struct{} type Y struct{} - type Config struct { - MakeX func(*Y) *X - MakeY func(*X) *Y - } - type Result struct { - X *X - Y *Y - } + sentinel := errors.New("boom") - cfg := Config{ - MakeX: func(y *Y) *X { - return &X{} + tests := []struct { + name string + config interface{} + result interface{} + expectedErrors []string + verify func(t *testing.T, result interface{}) + }{ + { + name: "constructor error propagation", + config: struct { + MakeA func() (*A, error) + }{ + MakeA: func() (*A, error) { + return nil, sentinel + }, + }, + result: &struct { + A *A + }{}, + expectedErrors: []string{"MakeA", "boom"}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + A *A + }) + if res.A != nil { + t.Fatalf("result A should not be populated on constructor failure") + } + }, }, - MakeY: func(x *X) *Y { - return &Y{} + { + name: "missing dependency", + config: struct { + MakeB func(*A) (*B, error) + }{ + MakeB: func(a *A) (*B, error) { + return &B{a: a}, nil + }, + }, + result: &struct { + B *B + }{}, + expectedErrors: []string{"*di.A", "di.A"}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + B *B + }) + if res.B != nil { + t.Fatalf("result B should not be populated") + } + }, }, - } - - var res Result - err := Build(cfg, &res) - if err == nil { - t.Fatalf("expected cycle detection error") - } - // Both constructors should still be listed as remaining. - if !strings.Contains(err.Error(), "MakeX") || !strings.Contains(err.Error(), "MakeY") { - t.Fatalf("expected error to list remaining constructors MakeX and MakeY, got: %v", err) - } - if res.X != nil || res.Y != nil { - t.Fatalf("cycle should prevent any construction; got X=%v Y=%v", res.X, res.Y) - } -} - -// Ensure that providing pre-supplied value satisfies dependency without constructor for it. -func TestBuildWithPreSuppliedValue(t *testing.T) { - type A struct { - v int - } - type B struct { - a *A - } - - type Config struct { - // Only constructor for B; A provided directly in config. - A *A - MB func(*A) (*B, error) - } - type Result struct { - A *A - B *B - } - - cfg := Config{ - A: &A{v: 42}, - MB: func(a *A) (*B, error) { - return &B{a: a}, nil + { + name: "cycle detection", + config: struct { + MakeX func(*Y) *X + MakeY func(*X) *Y + }{ + MakeX: func(y *Y) *X { + return &X{} + }, + MakeY: func(x *X) *Y { + return &Y{} + }, + }, + result: &struct { + X *X + Y *Y + }{}, + expectedErrors: []string{"MakeX", "MakeY"}, + verify: func(t *testing.T, result interface{}) { + res := result.(*struct { + X *X + Y *Y + }) + if res.X != nil || res.Y != nil { + t.Fatalf("cycle should prevent any construction; got X=%v Y=%v", res.X, res.Y) + } + }, }, } - var res Result - err := Build(cfg, &res) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res.A == nil || res.A.v != 42 { - t.Fatalf("expected pre-supplied A (42), got %+v", res.A) - } - if res.B == nil || res.B.a != res.A { - t.Fatalf("expected B referencing A, got %+v", res.B) - } -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Build(tt.config, tt.result) + if err == nil { + t.Fatalf("expected error") + } -func TestTypeAlias(t *testing.T) { - type ANum int - type Config struct { - A ANum - B int - } + errorStr := err.Error() + var foundError bool + for _, expectedErr := range tt.expectedErrors { + if strings.Contains(errorStr, expectedErr) { + foundError = true + break + } + } + if !foundError { + t.Fatalf("expected error to contain one of %v, got: %v", tt.expectedErrors, err) + } - type Result struct { - A ANum - } - var res Result - err := Build(Config{3, 4}, &res) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res.A != 3 { - t.Fatalf("expected A=3, got %v", res.A) + tt.verify(t, tt.result) + }) } } -func TestReferenceConfig(t *testing.T) { - type NestedConfig struct { - OtherSetting bool - NestedDecision func(c NestedConfig) uint - } - - type Config struct { - NestedConfig - SomeSetting bool - Inner func(c Config) int - } +func TestNewFunction(t *testing.T) { + type ANum int - type Result struct { - A int - B uint - } - var res Result - err := Build(Config{ - SomeSetting: true, - Inner: func(c Config) int { - if c.SomeSetting { - return 1 - } - return 0 + tests := []struct { + name string + config interface{} + expected interface{} + }{ + { + name: "struct result", + config: struct { + A int + }{A: 42}, + expected: struct { + A int + }{A: 42}, }, - NestedConfig: NestedConfig{ - OtherSetting: true, - NestedDecision: func(c NestedConfig) uint { - if c.OtherSetting { - return 1 - } - return 0 - }, + { + name: "primitive type result", + config: struct{ A int }{A: 42}, + expected: 42, }, - }, &res) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res.A != 1 { - t.Fatalf("expected A=1, got %v", res.A) } - if res.B != 1 { - t.Fatalf("expected B=1, got %v", res.A) - } -} -func TestNew(t *testing.T) { - type Config struct { - A int - } - - type Result struct { - A int - } - - cfg := Config{A: 42} - res, err := New[Result](cfg) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res.A != 42 { - t.Fatalf("expected A=42, got %v", res.A) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch expected := tt.expected.(type) { + case struct{ A int }: + res, err := New[struct{ A int }](tt.config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.A != expected.A { + t.Fatalf("expected A=%d, got %v", expected.A, res.A) + } + case int: + res, err := New[int](tt.config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != expected { + t.Fatalf("expected %d, got %v", expected, res) + } + } + }) } } -func TestSpecificTypes(t *testing.T) { - type Config struct { - A int - } - - cfg := Config{A: 42} - res, err := New[int](cfg) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res != 42 { - t.Fatalf("expected A=42, got %v", res) +func TestBuildPrimitiveTypes(t *testing.T) { + tests := []struct { + name string + config interface{} + expected int + }{ + { + name: "build primitive directly", + config: struct{ A int }{A: 42}, + expected: 42, + }, } - var res2 int - err = Build(Config{A: 42}, &res2) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if res2 != 42 { - t.Fatalf("expected A=42, got %v", res2) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var res int + err := Build(tt.config, &res) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != tt.expected { + t.Fatalf("expected %d, got %v", tt.expected, res) + } + }) } }