zoobzio December 12, 2025 Edit this page

Testing

ASTQL queries are testable without a database connection. This guide covers testing patterns and best practices.

Testing Query Output

Basic Output Testing

func TestUserQuery(t *testing.T) {
    instance := setupTestInstance()

    result, err := astql.Select(instance.T("users")).
        Fields(instance.F("username"), instance.F("email")).
        Where(instance.C(instance.F("active"), astql.EQ, instance.P("is_active"))).
        Render(postgres.New())

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    expected := `SELECT "username", "email" FROM "users" WHERE "active" = :is_active`
    if result.SQL != expected {
        t.Errorf("SQL mismatch\ngot:  %s\nwant: %s", result.SQL, expected)
    }

    if len(result.RequiredParams) != 1 || result.RequiredParams[0] != "is_active" {
        t.Errorf("params mismatch: got %v", result.RequiredParams)
    }
}

Test Instance Setup

Create a dedicated schema for tests:

func setupTestInstance() *astql.ASTQL {
    project := dbml.NewProject("test")

    users := dbml.NewTable("users")
    users.AddColumn(dbml.NewColumn("id", "bigint"))
    users.AddColumn(dbml.NewColumn("username", "varchar"))
    users.AddColumn(dbml.NewColumn("email", "varchar"))
    users.AddColumn(dbml.NewColumn("active", "boolean"))
    project.AddTable(users)

    posts := dbml.NewTable("posts")
    posts.AddColumn(dbml.NewColumn("id", "bigint"))
    posts.AddColumn(dbml.NewColumn("user_id", "bigint"))
    posts.AddColumn(dbml.NewColumn("title", "varchar"))
    posts.AddColumn(dbml.NewColumn("published", "boolean"))
    project.AddTable(posts)

    instance, err := astql.NewFromDBML(project)
    if err != nil {
        panic(err)
    }
    return instance
}

Table-Driven Tests

Testing Multiple Queries

func TestQueries(t *testing.T) {
    instance := setupTestInstance()

    tests := []struct {
        name     string
        query    func() (*astql.QueryResult, error)
        wantSQL  string
        wantParams []string
    }{
        {
            name: "simple select",
            query: func() (*astql.QueryResult, error) {
                return astql.Select(instance.T("users")).
                    Fields(instance.F("username")).
                    Render(postgres.New())
            },
            wantSQL:    `SELECT "username" FROM "users"`,
            wantParams: nil,
        },
        {
            name: "select with where",
            query: func() (*astql.QueryResult, error) {
                return astql.Select(instance.T("users")).
                    Where(instance.C(instance.F("id"), astql.EQ, instance.P("id"))).
                    Render(postgres.New())
            },
            wantSQL:    `SELECT * FROM "users" WHERE "id" = :id`,
            wantParams: []string{"id"},
        },
        {
            name: "select with order and limit",
            query: func() (*astql.QueryResult, error) {
                return astql.Select(instance.T("users")).
                    OrderBy(instance.F("username"), astql.ASC).
                    Limit(10).
                    Render(postgres.New())
            },
            wantSQL:    `SELECT * FROM "users" ORDER BY "username" ASC LIMIT 10`,
            wantParams: nil,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := tt.query()
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if result.SQL != tt.wantSQL {
                t.Errorf("SQL mismatch\ngot:  %s\nwant: %s", result.SQL, tt.wantSQL)
            }
            if !slicesEqual(result.RequiredParams, tt.wantParams) {
                t.Errorf("params mismatch\ngot:  %v\nwant: %v", result.RequiredParams, tt.wantParams)
            }
        })
    }
}

func slicesEqual(a, b []string) bool {
    if len(a) != len(b) {
        return false
    }
    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }
    return true
}

Testing Validation

Testing Invalid Input

func TestInvalidTable(t *testing.T) {
    instance := setupTestInstance()

    defer func() {
        if r := recover(); r == nil {
            t.Error("expected panic for invalid table")
        }
    }()

    instance.T("nonexistent")
}

func TestInvalidField(t *testing.T) {
    instance := setupTestInstance()

    defer func() {
        if r := recover(); r == nil {
            t.Error("expected panic for invalid field")
        }
    }()

    instance.F("nonexistent")
}

Testing Try Variants

func TestTryT(t *testing.T) {
    instance := setupTestInstance()

    // Valid table
    table, err := instance.TryT("users")
    if err != nil {
        t.Errorf("unexpected error for valid table: %v", err)
    }
    if table.Name != "users" {
        t.Errorf("wrong table name: %s", table.Name)
    }

    // Invalid table
    _, err = instance.TryT("nonexistent")
    if err == nil {
        t.Error("expected error for invalid table")
    }
}

Testing Error Cases

Builder Errors

func TestBuilderErrors(t *testing.T) {
    instance := setupTestInstance()

    // Fields on non-SELECT query
    _, err := astql.Insert(instance.T("users")).
        Fields(instance.F("username")).
        Render(postgres.New())

    if err == nil {
        t.Error("expected error for Fields on INSERT")
    }

    // SET on non-UPDATE query
    _, err = astql.Select(instance.T("users")).
        Set(instance.F("username"), instance.P("value")).
        Render(postgres.New())

    if err == nil {
        t.Error("expected error for Set on SELECT")
    }
}

Testing Complex Queries

Join Tests

func TestJoinQuery(t *testing.T) {
    instance := setupTestInstance()

    result, err := astql.Select(instance.T("users", "u")).
        Fields(
            instance.WithTable(instance.F("username"), "u"),
            instance.WithTable(instance.F("title"), "p"),
        ).
        LeftJoin(instance.T("posts", "p"), astql.CF(
            instance.WithTable(instance.F("id"), "u"),
            astql.EQ,
            instance.WithTable(instance.F("user_id"), "p"),
        )).
        Render(postgres.New())

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // Check key parts of the query
    if !strings.Contains(result.SQL, "LEFT JOIN") {
        t.Error("missing LEFT JOIN")
    }
    if !strings.Contains(result.SQL, `u."id" = p."user_id"`) {
        t.Error("missing join condition")
    }
}

Subquery Tests

func TestSubquery(t *testing.T) {
    instance := setupTestInstance()

    subquery := astql.Sub(
        astql.Select(instance.T("posts")).
            Fields(instance.F("user_id")).
            Where(instance.C(instance.F("published"), astql.EQ, instance.P("is_pub"))),
    )

    result, err := astql.Select(instance.T("users")).
        Where(astql.CSub(instance.F("id"), astql.IN, subquery)).
        Render(postgres.New())

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // Check parameter namespacing
    if !strings.Contains(result.SQL, ":sq1_is_pub") {
        t.Error("subquery parameter not namespaced")
    }
}

Snapshot Testing

For complex queries, consider snapshot testing:

func TestComplexQuery_Snapshot(t *testing.T) {
    instance := setupTestInstance()

    result, err := buildComplexQuery(instance)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    golden := filepath.Join("testdata", t.Name()+".golden.sql")

    if *update {
        os.WriteFile(golden, []byte(result.SQL), 0644)
        return
    }

    expected, err := os.ReadFile(golden)
    if err != nil {
        t.Fatalf("failed to read golden file: %v", err)
    }

    if result.SQL != string(expected) {
        t.Errorf("SQL mismatch with golden file\ngot:\n%s", result.SQL)
    }
}

Best Practices

1. Test Query Structure, Not Exact Strings

For complex queries, test for required components:

func TestQueryContains(t *testing.T) {
    result, _ := buildQuery()

    checks := []string{
        `FROM "users"`,
        `WHERE "active"`,
        `ORDER BY`,
        `LIMIT`,
    }

    for _, check := range checks {
        if !strings.Contains(result.SQL, check) {
            t.Errorf("missing: %s", check)
        }
    }
}

2. Test Parameter Lists

Always verify required parameters:

func TestParams(t *testing.T) {
    result, _ := buildQuery()

    expected := []string{"user_id", "status", "limit"}
    for _, param := range expected {
        found := false
        for _, p := range result.RequiredParams {
            if p == param {
                found = true
                break
            }
        }
        if !found {
            t.Errorf("missing param: %s", param)
        }
    }
}

3. Isolate Test Schemas

Each test file or package should have its own schema setup:

// users_test.go
func setupUsersTestInstance() *astql.ASTQL { ... }

// orders_test.go
func setupOrdersTestInstance() *astql.ASTQL { ... }

4. Test Edge Cases

func TestEdgeCases(t *testing.T) {
    instance := setupTestInstance()

    // Empty fields (SELECT *)
    result, _ := astql.Select(instance.T("users")).Render(postgres.New())
    if !strings.Contains(result.SQL, "SELECT *") {
        t.Error("empty fields should produce SELECT *")
    }

    // Multiple WHERE calls
    result, _ = astql.Select(instance.T("users")).
        Where(instance.C(instance.F("active"), astql.EQ, instance.P("a"))).
        Where(instance.C(instance.F("id"), astql.EQ, instance.P("b"))).
        Render(postgres.New())
    if !strings.Contains(result.SQL, "AND") {
        t.Error("multiple WHERE should combine with AND")
    }
}