On this page

Test your data layer

Prove that a failed transaction leaves the task table unchanged. The test will check both the error and the surviving data.

Starting point

Continue in the practice application after migrations.

Step 1: Define the failure

Add "errors" to the imports in main.go:

"errors"

Append this code before the final return nil in run:

	stop := errors.New("roll back this transaction")
	err = queries.RunInTx(ctx, db, func(tx *store.Queries) error {
		if _, err := tx.CreateTask(ctx, "Do not keep this task"); err != nil {
			return err
		}
		return stop
	})
	if !errors.Is(err, stop) {
		return fmt.Errorf("expected rollback error, got %v", err)
	}
	tasks, err := queries.ListTasks(ctx)
	if err != nil {
		return err
	}
	fmt.Fprintln(out, "tasks after rollback:", len(tasks))

The callback inserts a second task, then returns stop. RunInTx rolls the transaction back. The caller checks that error and counts tasks through the outer connection.

go run .
1 Feed the seagulls
Bring chips
missing: true
Feed the seagulls in Harbour
priority: 1
tasks after rollback: 1

Only the original task remains.

Step 2: Check it in a test

Create main_test.go:

package main

import (
    "bytes"
    "path/filepath"
    "strings"
    "testing"

    "seagull.sh/seagull/tests/tutorials/internal/support"
)

func TestRollback(t *testing.T) {
    db, err := support.Open(t.Context(), migrations, filepath.Join(t.TempDir(), "tasks.db"))
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()
    var output bytes.Buffer
    if err := run(t.Context(), db, &output); err != nil {
        t.Fatal(err)
    }
    if !strings.HasSuffix(output.String(), "tasks after rollback: 1\n") {
        t.Fatalf("unexpected surviving tasks:\n%s", output.String())
    }
}

The temporary database keeps each test independent. If the callback returns the wrong error, run fails. If the inserted task survives, the final assertion fails.

go test . -count=1

Step 3: See the test fail

Inside the callback, replace return stop with return nil:

return nil // commit for this experiment

Run the test again:

go test . -count=1

Its failure includes:

expected rollback error

A successful callback committed the transaction instead of requesting rollback. Restore the original return:

return stop
go test . -count=1

Continue with CI checks. Use the same approach against your production database when testing behaviour that differs between engines.