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")
}
}