zoobzio January 3, 2026 Edit this page

Testing

Edamame provides testing utilities in the github.com/zoobz-io/edamame/testing package.

Test Helpers

QueryCapture

Capture rendered SQL for verification:

import (
    edamametesting "github.com/zoobz-io/edamame/testing"
    "github.com/zoobz-io/astql/pkg/postgres"
)

func TestQueryRendering(t *testing.T) {
    capture := edamametesting.NewQueryCapture()

    exec, _ := edamame.New[User](nil, "users", postgres.New())

    // Define a statement
    var ByStatus = edamame.NewQueryStatement("by-status", "Find by status", edamame.QuerySpec{
        Where: []edamame.ConditionSpec{{Field: "status", Operator: "=", Param: "status"}},
    })

    // Get the builder and render
    q, _ := exec.Query(ByStatus)
    result, _ := q.Render()

    capture.CaptureQuery("by-status", "query", result.SQL, nil)

    // Verify
    if capture.Count() != 1 {
        t.Errorf("expected 1 query, got %d", capture.Count())
    }

    last := capture.Last()
    if last.Type != "query" {
        t.Errorf("expected type 'query', got %q", last.Type)
    }
}

ExecutorEventCapture

Capture executor creation events via capitan:

func TestExecutorEvents(t *testing.T) {
    c := capitan.New(capitan.WithSyncMode())
    defer c.Shutdown()

    capture := edamametesting.NewExecutorEventCapture()
    c.Hook(edamame.ExecutorCreated, capture.Handler())

    exec, _ := edamame.New[User](nil, "users", postgres.New())

    // Verify event captured
    if capture.Count() != 1 {
        t.Error("expected executor created event")
    }

    tables := capture.Tables()
    if tables[0].Table != "users" {
        t.Errorf("expected table 'users', got %q", tables[0].Table)
    }
}

ParamBuilder

Build test parameters fluently:

func TestWithParams(t *testing.T) {
    params := edamametesting.NewParamBuilder().
        Set("id", 123).
        Set("status", "active").
        Set("limit", 10).
        Build()

    var Filtered = edamame.NewQueryStatement("filtered", "Filtered query", edamame.QuerySpec{
        Where: []edamame.ConditionSpec{{Field: "status", Operator: "=", Param: "status"}},
    })

    // Use in tests
    users, err := exec.ExecQuery(ctx, Filtered, params)
}

Unit Testing Without Database

Test statement creation and specs without a database:

func TestStatements(t *testing.T) {
    // nil db is valid for testing statements
    exec, err := edamame.New[User](nil, "users", postgres.New())
    if err != nil {
        t.Fatal(err)
    }

    // Define statements
    var QueryAll = edamame.NewQueryStatement("query-all", "Query all", edamame.QuerySpec{})

    var ByStatus = edamame.NewQueryStatement("by-status", "Find by status", edamame.QuerySpec{
        Where: []edamame.ConditionSpec{
            {Field: "status", Operator: "=", Param: "status"},
        },
    })

    // Test statement metadata
    if ByStatus.Name() != "by-status" {
        t.Errorf("expected name 'by-status', got %q", ByStatus.Name())
    }

    // Test params derived correctly
    params := ByStatus.Params()
    if len(params) != 1 || params[0].Name != "status" {
        t.Error("expected 1 param named 'status'")
    }

    // Test builder creation
    q, err := exec.Query(ByStatus)
    if err != nil {
        t.Fatal(err)
    }

    // Test rendering
    result, err := q.Render()
    if err != nil {
        t.Fatal(err)
    }

    if result.SQL == "" {
        t.Error("empty SQL rendered")
    }
}

Testing SQL Rendering

Verify generated SQL without executing:

func TestSQLRendering(t *testing.T) {
    exec, _ := edamame.New[User](nil, "users", postgres.New())

    var Adults = edamame.NewQueryStatement("adults", "Find adults", edamame.QuerySpec{
        Where: []edamame.ConditionSpec{
            {Field: "age", Operator: ">=", Param: "min_age"},
        },
        OrderBy: []edamame.OrderBySpec{
            {Field: "name", Direction: "asc"},
        },
    })

    q, _ := exec.Query(Adults)
    result, err := q.Render()
    if err != nil {
        t.Fatal(err)
    }

    // Verify SQL structure
    if !strings.Contains(result.SQL, "WHERE") {
        t.Error("SQL missing WHERE clause")
    }
    if !strings.Contains(result.SQL, "ORDER BY") {
        t.Error("SQL missing ORDER BY clause")
    }
}

Integration Testing with Testcontainers

For database integration tests, use testcontainers:

//go:build integration

package integration

import (
    "context"
    "testing"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func TestWithPostgres(t *testing.T) {
    ctx := context.Background()

    // Start PostgreSQL container
    req := testcontainers.ContainerRequest{
        Image:        "postgres:16-alpine",
        ExposedPorts: []string{"5432/tcp"},
        WaitingFor:   wait.ForLog("database system is ready to accept connections"),
        Env: map[string]string{
            "POSTGRES_USER":     "test",
            "POSTGRES_PASSWORD": "test",
            "POSTGRES_DB":       "testdb",
        },
    }

    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatal(err)
    }
    defer container.Terminate(ctx)

    // Get connection details
    host, _ := container.Host(ctx)
    port, _ := container.MappedPort(ctx, "5432")

    // Connect and test
    dsn := fmt.Sprintf("host=%s port=%s user=test password=test dbname=testdb sslmode=disable", host, port.Port())
    db, err := sqlx.Connect("postgres", dsn)
    if err != nil {
        t.Fatal(err)
    }

    // Create table
    db.ExecContext(ctx, `CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        email TEXT NOT NULL UNIQUE,
        name TEXT,
        age INTEGER
    )`)

    // Test executor
    exec, _ := edamame.New[User](db, "users", postgres.New())

    age := 25
    user := &User{Email: "test@example.com", Name: "Test", Age: &age}
    inserted, err := exec.ExecInsert(ctx, user)
    if err != nil {
        t.Fatal(err)
    }

    if inserted.ID == 0 {
        t.Error("expected non-zero ID")
    }
}

Run integration tests:

go test -tags=integration ./testing/integration/...

Benchmarking

The testing/benchmarks package provides performance benchmarks:

func BenchmarkQueryBuilding(b *testing.B) {
    exec, _ := edamame.New[User](nil, "users", postgres.New())

    var QueryAll = edamame.NewQueryStatement("query-all", "Query all", edamame.QuerySpec{})

    b.ResetTimer()
    b.ReportAllocs()

    for i := 0; i < b.N; i++ {
        _, _ = exec.Query(QueryAll)
    }
}

Run benchmarks:

go test ./testing/benchmarks/... -bench=. -benchmem

Testing Events

Verify capitan events are emitted correctly:

func TestExecutorEmitsCreatedEvent(t *testing.T) {
    c := capitan.New(capitan.WithSyncMode())
    defer c.Shutdown()

    capture := edamametesting.NewExecutorEventCapture()
    c.Hook(edamame.ExecutorCreated, capture.Handler())

    _, _ = edamame.New[User](nil, "users", postgres.New())

    if capture.Count() != 1 {
        t.Errorf("expected 1 executor created event, got %d", capture.Count())
    }

    tables := capture.Tables()
    if tables[0].Table != "users" {
        t.Errorf("expected table 'users', got %q", tables[0].Table)
    }
}

Testing Transaction Behavior

func TestTransaction(t *testing.T) {
    // ... setup db and exec ...

    var QueryAll = edamame.NewQueryStatement("query-all", "Query all", edamame.QuerySpec{})
    var SelectByID = edamame.NewSelectStatement("select-by-id", "Select by ID", edamame.SelectSpec{
        Where: []edamame.ConditionSpec{{Field: "id", Operator: "=", Param: "id"}},
    })

    tx, _ := db.BeginTxx(ctx, nil)

    // Insert in transaction
    user := &User{Email: "tx@test.com", Name: "TxTest"}
    inserted, err := exec.ExecInsertTx(ctx, tx, user)
    if err != nil {
        tx.Rollback()
        t.Fatal(err)
    }

    // Verify visible in transaction
    users, _ := exec.ExecQueryTx(ctx, tx, QueryAll, nil)
    if len(users) != 1 {
        t.Error("expected 1 user in transaction")
    }

    // Rollback
    tx.Rollback()

    // Verify not visible after rollback
    users, _ = exec.ExecQuery(ctx, QueryAll, nil)
    if len(users) != 0 {
        t.Error("expected 0 users after rollback")
    }
}

Async Event Testing

Use WaitForCount for async event verification:

func TestAsyncEvents(t *testing.T) {
    c := capitan.New()  // async mode
    defer c.Shutdown()

    capture := edamametesting.NewExecutorEventCapture()
    c.Hook(edamame.ExecutorCreated, capture.Handler())

    _, _ = edamame.New[User](nil, "users", postgres.New())

    // Wait for async event processing
    if !capture.WaitForCount(1, 500*time.Millisecond) {
        t.Error("timed out waiting for event")
    }
}