zoobzio January 5, 2025 Edit this page

Testing

Testing strategies for atom-based code.

Basic Testing

Round-Trip Tests

Verify that atomize/deatomize preserves data:

func TestUserRoundTrip(t *testing.T) {
    atomizer, err := atom.Use[User]()
    if err != nil {
        t.Fatalf("Use failed: %v", err)
    }

    original := &User{
        Name:   "Alice",
        Age:    30,
        Active: true,
    }

    // Atomize
    a := atomizer.Atomize(original)

    // Deatomize
    restored, err := atomizer.Deatomize(a)
    if err != nil {
        t.Fatalf("Deatomize failed: %v", err)
    }

    // Compare
    if restored.Name != original.Name {
        t.Errorf("Name: got %q, want %q", restored.Name, original.Name)
    }
    if restored.Age != original.Age {
        t.Errorf("Age: got %d, want %d", restored.Age, original.Age)
    }
    if restored.Active != original.Active {
        t.Errorf("Active: got %v, want %v", restored.Active, original.Active)
    }
}

Using reflect.DeepEqual

For complex structs:

func TestComplexRoundTrip(t *testing.T) {
    atomizer, _ := atom.Use[ComplexType]()

    original := &ComplexType{
        // ... many fields
    }

    a := atomizer.Atomize(original)
    restored, err := atomizer.Deatomize(a)
    if err != nil {
        t.Fatalf("Deatomize failed: %v", err)
    }

    if !reflect.DeepEqual(original, restored) {
        t.Errorf("round-trip failed:\noriginal: %+v\nrestored: %+v",
            original, restored)
    }
}

Testing Specific Fields

Verify Atomization Output

func TestUserAtomization(t *testing.T) {
    atomizer, _ := atom.Use[User]()

    user := &User{Name: "Alice", Age: 30}
    a := atomizer.Atomize(user)

    // Check specific fields
    if got := a.Strings["Name"]; got != "Alice" {
        t.Errorf("Strings[Name]: got %q, want %q", got, "Alice")
    }
    if got := a.Ints["Age"]; got != 30 {
        t.Errorf("Ints[Age]: got %d, want %d", got, 30)
    }
}

Verify Deatomization Input

func TestUserDeatomization(t *testing.T) {
    atomizer, _ := atom.Use[User]()

    a := &atom.Atom{
        Strings: map[string]string{"Name": "Bob"},
        Ints:    map[string]int64{"Age": 25},
    }

    user, err := atomizer.Deatomize(a)
    if err != nil {
        t.Fatalf("Deatomize failed: %v", err)
    }

    if user.Name != "Bob" {
        t.Errorf("Name: got %q, want %q", user.Name, "Bob")
    }
    if user.Age != 25 {
        t.Errorf("Age: got %d, want %d", user.Age, 25)
    }
}

Table-Driven Tests

Testing Multiple Cases

func TestUserRoundTrips(t *testing.T) {
    atomizer, _ := atom.Use[User]()

    tests := []struct {
        name string
        user User
    }{
        {"empty", User{}},
        {"basic", User{Name: "Alice", Age: 30}},
        {"all fields", User{Name: "Bob", Age: 25, Active: true}},
        {"unicode", User{Name: "日本語", Age: 100}},
        {"max age", User{Name: "Elder", Age: math.MaxInt64}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            a := atomizer.Atomize(&tt.user)
            restored, err := atomizer.Deatomize(a)
            if err != nil {
                t.Fatalf("Deatomize failed: %v", err)
            }
            if !reflect.DeepEqual(&tt.user, restored) {
                t.Errorf("mismatch:\noriginal: %+v\nrestored: %+v",
                    tt.user, restored)
            }
        })
    }
}

Testing Error Cases

Overflow Detection

func TestOverflowDetection(t *testing.T) {
    type Small struct {
        Value int8
    }

    atomizer, _ := atom.Use[Small]()

    tests := []struct {
        name    string
        value   int64
        wantErr bool
    }{
        {"valid positive", 100, false},
        {"valid negative", -100, false},
        {"overflow positive", 200, true},
        {"overflow negative", -200, true},
        {"max", 127, false},
        {"min", -128, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            a := &atom.Atom{Ints: map[string]int64{"Value": tt.value}}
            _, err := atomizer.Deatomize(a)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr = %v", err, tt.wantErr)
            }
        })
    }
}

Unsupported Types

func TestUnsupportedType(t *testing.T) {
    type Invalid struct {
        Data map[string]any
    }

    _, err := atom.Use[Invalid]()
    if err == nil {
        t.Fatal("expected error for unsupported type")
    }
    if !strings.Contains(err.Error(), "map types are not supported") {
        t.Errorf("unexpected error: %v", err)
    }
}

Testing Nested Structs

func TestNestedRoundTrip(t *testing.T) {
    type Address struct {
        Street string
        City   string
    }
    type User struct {
        Name    string
        Address Address
    }

    atomizer, _ := atom.Use[User]()

    original := &User{
        Name: "Alice",
        Address: Address{
            Street: "123 Main St",
            City:   "Springfield",
        },
    }

    a := atomizer.Atomize(original)

    // Verify nested structure
    if _, ok := a.Nested["Address"]; !ok {
        t.Fatal("expected Address in Nested map")
    }

    restored, err := atomizer.Deatomize(a)
    if err != nil {
        t.Fatalf("Deatomize failed: %v", err)
    }

    if restored.Address.Street != original.Address.Street {
        t.Errorf("Street: got %q, want %q",
            restored.Address.Street, original.Address.Street)
    }
}

Testing Custom Interfaces

type CustomUser struct {
    Name string
}

func (u *CustomUser) Atomize(a *atom.Atom) {
    a.Strings["Name"] = "custom:" + u.Name
}

func (u *CustomUser) Deatomize(a *atom.Atom) error {
    name := a.Strings["Name"]
    if !strings.HasPrefix(name, "custom:") {
        return fmt.Errorf("invalid format")
    }
    u.Name = strings.TrimPrefix(name, "custom:")
    return nil
}

func TestCustomInterface(t *testing.T) {
    atomizer, _ := atom.Use[CustomUser]()

    original := &CustomUser{Name: "Alice"}
    a := atomizer.Atomize(original)

    // Verify custom atomization was used
    if got := a.Strings["Name"]; got != "custom:Alice" {
        t.Errorf("expected custom format, got %q", got)
    }

    restored, err := atomizer.Deatomize(a)
    if err != nil {
        t.Fatalf("Deatomize failed: %v", err)
    }

    if restored.Name != original.Name {
        t.Errorf("Name: got %q, want %q", restored.Name, original.Name)
    }
}

Concurrent Testing

func TestConcurrentUse(t *testing.T) {
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomizer, err := atom.Use[User]()
            if err != nil {
                t.Errorf("Use failed: %v", err)
                return
            }
            _ = atomizer.Atomize(&User{Name: "test"})
        }()
    }

    wg.Wait()
}

Benchmarking

func BenchmarkAtomize(b *testing.B) {
    atomizer, _ := atom.Use[User]()
    user := &User{Name: "Alice", Age: 30, Active: true}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = atomizer.Atomize(user)
    }
}

func BenchmarkDeatomize(b *testing.B) {
    atomizer, _ := atom.Use[User]()
    a := atomizer.Atomize(&User{Name: "Alice", Age: 30, Active: true})

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = atomizer.Deatomize(a)
    }
}

func BenchmarkRoundTrip(b *testing.B) {
    atomizer, _ := atom.Use[User]()
    user := &User{Name: "Alice", Age: 30, Active: true}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        a := atomizer.Atomize(user)
        _, _ = atomizer.Deatomize(a)
    }
}

Test Helpers

See Testing Reference for the testing package utilities:

  • AtomBuilder - Fluent atom construction
  • AtomMatcher - Deep comparison
  • RoundTripValidator - Automated round-trip testing

Best Practices

Test at Registration

func TestMain(m *testing.M) {
    // Verify all types register successfully
    types := []func() error{
        func() error { _, err := atom.Use[User](); return err },
        func() error { _, err := atom.Use[Order](); return err },
    }

    for _, register := range types {
        if err := register(); err != nil {
            log.Fatalf("registration failed: %v", err)
        }
    }

    os.Exit(m.Run())
}

Test Edge Cases

  • Empty structs
  • Nil pointers
  • Empty slices vs nil slices
  • Maximum/minimum values
  • Unicode strings
  • Large byte slices

Next Steps