zoobzio January 5, 2025 Edit this page

Testing Reference

Test utilities for atom-based applications.

Overview

The atom/testing package provides utilities for testing code that uses atom.

import atomtest "github.com/zoobz-io/atom/testing"

AtomBuilder

Fluent builder for constructing test atoms.

Constructor

func NewAtomBuilder() *AtomBuilder

Methods

// Scalar types
func (b *AtomBuilder) String(key, value string) *AtomBuilder
func (b *AtomBuilder) Int(key string, value int64) *AtomBuilder
func (b *AtomBuilder) Uint(key string, value uint64) *AtomBuilder
func (b *AtomBuilder) Float(key string, value float64) *AtomBuilder
func (b *AtomBuilder) Bool(key string, value bool) *AtomBuilder
func (b *AtomBuilder) Time(key string, value time.Time) *AtomBuilder
func (b *AtomBuilder) Bytes(key string, value []byte) *AtomBuilder

// Pointer types
func (b *AtomBuilder) StringPtr(key string, value *string) *AtomBuilder
func (b *AtomBuilder) IntPtr(key string, value *int64) *AtomBuilder

// Slice types
func (b *AtomBuilder) StringSlice(key string, value []string) *AtomBuilder
func (b *AtomBuilder) IntSlice(key string, value []int64) *AtomBuilder

// Nested types
func (b *AtomBuilder) Nested(key string, value *atom.Atom) *AtomBuilder
func (b *AtomBuilder) NestedSlice(key string, value []atom.Atom) *AtomBuilder

// Metadata
func (b *AtomBuilder) WithSpec(spec atom.Spec) *AtomBuilder
func (b *AtomBuilder) Build() *atom.Atom

Example

a := atomtest.NewAtomBuilder().
    String("Name", "Alice").
    Int("Age", 30).
    Bool("Active", true).
    Build()

user, _ := atomizer.Deatomize(a)

AtomMatcher

Deep comparison utilities for atoms.

Functions

func Equal(a, b *atom.Atom) bool
func EqualFields(a, b *atom.Atom, fields ...string) bool
func Diff(a, b *atom.Atom) string

Example

func TestAtomization(t *testing.T) {
    original := &User{Name: "Alice", Age: 30}
    a := atomizer.Atomize(original)

    expected := atomtest.NewAtomBuilder().
        String("Name", "Alice").
        Int("Age", 30).
        Build()

    if !atomtest.Equal(a, expected) {
        t.Errorf("mismatch:\n%s", atomtest.Diff(a, expected))
    }
}

RoundTripValidator

Automated round-trip testing.

Constructor

func NewRoundTripValidator[T any](atomizer *atom.Atomizer[T]) *RoundTripValidator[T]

Methods

func (v *RoundTripValidator[T]) Validate(t *testing.T, original *T)
func (v *RoundTripValidator[T]) ValidateAll(t *testing.T, cases []*T)

Example

func TestUserRoundTrip(t *testing.T) {
    atomizer, _ := atom.Use[User]()
    validator := atomtest.NewRoundTripValidator(atomizer)

    cases := []*User{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
        {Name: "", Age: 0}, // Edge case
    }

    validator.ValidateAll(t, cases)
}

FieldChecker

Type-safe field value assertions.

Functions

func GetString(a *atom.Atom, key string) (string, bool)
func GetInt(a *atom.Atom, key string) (int64, bool)
func GetUint(a *atom.Atom, key string) (uint64, bool)
func GetFloat(a *atom.Atom, key string) (float64, bool)
func GetBool(a *atom.Atom, key string) (bool, bool)
func GetTime(a *atom.Atom, key string) (time.Time, bool)
func GetBytes(a *atom.Atom, key string) ([]byte, bool)

Example

func TestUserFields(t *testing.T) {
    a := atomizer.Atomize(&User{Name: "Alice", Age: 30})

    name, ok := atomtest.GetString(a, "Name")
    if !ok || name != "Alice" {
        t.Errorf("expected Name=Alice, got %s", name)
    }

    age, ok := atomtest.GetInt(a, "Age")
    if !ok || age != 30 {
        t.Errorf("expected Age=30, got %d", age)
    }
}

MustUse

Panics on registration failure (for test setup).

func MustUse[T any](t *testing.T) *atom.Atomizer[T]

Example

func TestUser(t *testing.T) {
    atomizer := atomtest.MustUse[User](t)

    // Test code...
}

TypeFixtures

Common test data generators.

Functions

func RandomString(length int) string
func RandomInt(min, max int64) int64
func RandomFloat(min, max float64) float64
func RandomBytes(length int) []byte
func RandomTime() time.Time

Example

func TestRandomUser(t *testing.T) {
    user := &User{
        Name:   atomtest.RandomString(10),
        Age:    atomtest.RandomInt(18, 100),
        Score:  atomtest.RandomFloat(0, 100),
    }

    validator.Validate(t, user)
}

AssertAtom

Testing assertions for atoms.

Functions

func AssertHasString(t *testing.T, a *atom.Atom, key, expected string)
func AssertHasInt(t *testing.T, a *atom.Atom, key string, expected int64)
func AssertHasFloat(t *testing.T, a *atom.Atom, key string, expected float64)
func AssertHasBool(t *testing.T, a *atom.Atom, key string, expected bool)
func AssertHasNested(t *testing.T, a *atom.Atom, key string)
func AssertMissingField(t *testing.T, a *atom.Atom, table atom.Table, key string)

Example

func TestUserAtom(t *testing.T) {
    a := atomizer.Atomize(&User{Name: "Alice", Age: 30})

    atomtest.AssertHasString(t, a, "Name", "Alice")
    atomtest.AssertHasInt(t, a, "Age", 30)
    atomtest.AssertMissingField(t, a, atom.TableStrings, "NotAField")
}

Example Test File

package myapp

import (
    "testing"

    "github.com/zoobz-io/atom"
    atomtest "github.com/zoobz-io/atom/testing"
)

func TestUserAtomization(t *testing.T) {
    atomizer := atomtest.MustUse[User](t)

    t.Run("basic round trip", func(t *testing.T) {
        original := &User{Name: "Alice", Age: 30}
        a := atomizer.Atomize(original)

        atomtest.AssertHasString(t, a, "Name", "Alice")
        atomtest.AssertHasInt(t, a, "Age", 30)

        restored, err := atomizer.Deatomize(a)
        if err != nil {
            t.Fatal(err)
        }

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

    t.Run("builder pattern", func(t *testing.T) {
        a := atomtest.NewAtomBuilder().
            String("Name", "Bob").
            Int("Age", 25).
            Build()

        user, err := atomizer.Deatomize(a)
        if err != nil {
            t.Fatal(err)
        }

        if user.Name != "Bob" {
            t.Errorf("expected Bob, got %s", user.Name)
        }
    })

    t.Run("random data", func(t *testing.T) {
        validator := atomtest.NewRoundTripValidator(atomizer)

        for i := 0; i < 100; i++ {
            user := &User{
                Name: atomtest.RandomString(20),
                Age:  atomtest.RandomInt(0, 150),
            }
            validator.Validate(t, user)
        }
    })
}

Best Practices

Use MustUse in Tests

// Good - fails test on error
atomizer := atomtest.MustUse[User](t)

// Verbose - manual error handling
atomizer, err := atom.Use[User]()
if err != nil {
    t.Fatalf("Use failed: %v", err)
}

Use Builders for Clarity

// Good - clear intent
a := atomtest.NewAtomBuilder().
    String("Name", "Alice").
    Int("Age", 30).
    Build()

// Less clear
a := &atom.Atom{
    Strings: map[string]string{"Name": "Alice"},
    Ints:    map[string]int64{"Age": 30},
}

Use Validators for Round-Trips

// Good - automated validation
validator.ValidateAll(t, testCases)

// Manual - error-prone
for _, tc := range testCases {
    a := atomizer.Atomize(tc)
    restored, _ := atomizer.Deatomize(a)
    // Manual comparison...
}