Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Isolated database tests with samurai

This example demonstrates writing fully isolated PostgreSQL tests using samurai and pgtestdb. Each leaf test gets its own database — no shared state, full parallel execution.

How it works

Samurai re-runs the builder from scratch for each leaf path. Database setup lives in a parent Test callback, so every leaf automatically gets a fresh, isolated database:

Path 1: with fresh database → Add → returns sequential IDs     (own DB)
Path 2: with fresh database → Get → returns the created todo   (own DB)
Path 3: with fresh database → Get → returns error for non-existent ID  (own DB)
Path 4: with fresh database → Complete → marks todo as done    (own DB)
Path 5: with fresh database → All → returns empty list         (own DB)
Path 6: with fresh database → All → returns all added todos    (own DB)

All 6 paths run in parallel. Each calls newPool() which provisions a fresh PostgreSQL database with migrations already applied.

pgtestdb makes this fast by using PostgreSQL template databases: migrations run once, then each test gets a near-instant clone (~20ms). But samurai itself has no dependency on pgtestdb — the same pattern works with testcontainers-go, custom scripts, or any other approach that gives each test its own database.

The pattern

A helper provisions a fresh database and returns a connection pool:

func newPool(ctx context.Context, t *testing.T) *pgxpool.Pool {
    t.Helper()
    conf := pgtestdb.Custom(t, dbConf, migrator)
    pool, err := pgxpool.New(ctx, conf.URL())
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(pool.Close)
    return pool
}

pgtestdb.Custom() creates an isolated database with Atlas migrations applied and returns connection config. We use Custom() instead of New() because we need a native pgxpool.Pool, not *sql.DB.

The samurai test tree calls newPool in the parent setup — each leaf path re-executes this setup independently:

func TestTodoRepo(t *testing.T) {
    samurai.Run(t, func(s *samurai.Scope) {
        var repo *todo.Repo

        s.Test("with fresh database", func(ctx context.Context, w samurai.W) {
            pool := newPool(ctx, w.Testing())
            repo = todo.NewRepo(pool)
        }, func(s *samurai.Scope) {

            s.Test("Add", func(ctx context.Context, w samurai.W) {
                id, err := repo.Add(ctx, "buy milk")
                assert.NoError(w.Testing(), err)
                assert.Positive(w.Testing(), id)
            }, func(s *samurai.Scope) {
                s.Test("returns sequential IDs", func(ctx context.Context, w samurai.W) {
                    id1, _ := repo.Add(ctx, "first")
                    id2, _ := repo.Add(ctx, "second")
                    assert.Equal(w.Testing(), id1+1, id2)
                })
            })

            s.Test("Get", func(ctx context.Context, w samurai.W) {
                _, err := repo.Add(ctx, "learn samurai")
                assert.NoError(w.Testing(), err)
            }, func(s *samurai.Scope) {
                s.Test("returns the created todo", func(ctx context.Context, w samurai.W) {
                    id, _ := repo.Add(ctx, "test get")
                    got, err := repo.Get(ctx, id)
                    assert.NoError(w.Testing(), err)
                    assert.Equal(w.Testing(), "test get", got.Title)
                    assert.False(w.Testing(), got.Done)
                })

                s.Test("returns error for non-existent ID", func(ctx context.Context, w samurai.W) {
                    _, err := repo.Get(ctx, 99999)
                    assert.Error(w.Testing(), err)
                })
            })

            // ... more test groups (Complete, All)
        })
    })
}

Stack

Component Role
samurai Test framework — path isolation, parallel execution
pgtestdb Database provisioning — template-based fast cloning
Atlas Schema migrations
pgx PostgreSQL driver (native pgxpool.Pool)
testify Assertions

Prerequisites

  • Go 1.24+
  • Docker
  • Atlas CLI (curl -sSf https://atlasgo.sh | sh)

Run

docker compose up -d          # start postgres on port 5444
go test -v -count=1 ./...     # run tests
docker compose down            # cleanup

Output:

=== RUN   TestTodoRepo
=== RUN   TestTodoRepo/with_fresh_database
=== RUN   TestTodoRepo/with_fresh_database/Add
=== RUN   TestTodoRepo/with_fresh_database/Add/returns_sequential_IDs
=== RUN   TestTodoRepo/with_fresh_database/Get
=== RUN   TestTodoRepo/with_fresh_database/Get/returns_the_created_todo
=== RUN   TestTodoRepo/with_fresh_database/Get/returns_error_for_non-existent_ID
=== RUN   TestTodoRepo/with_fresh_database/Complete
=== RUN   TestTodoRepo/with_fresh_database/Complete/marks_todo_as_done
=== RUN   TestTodoRepo/with_fresh_database/All
=== RUN   TestTodoRepo/with_fresh_database/All/returns_empty_list_on_fresh_DB
=== RUN   TestTodoRepo/with_fresh_database/All/returns_all_added_todos
--- PASS: TestTodoRepo (0.84s)

Files

File Purpose
todo.go Simple Repo with Add/Get/Complete/All via pgxpool
todo_test.go Samurai tests demonstrating isolated DB testing
migrations/ Atlas migration creating the todos table
docker-compose.yml PostgreSQL 17 on port 5444 (tmpfs, fsync=off)