Documentation
¶
Overview ¶
Package samurai provides a scoped testing framework with path isolation for Go.
Samurai is inspired by GoConvey but modernized with:
- Explicit context passing (no GLS/goroutine-local-storage)
- Standard context.Context support for cancellation/timeouts
- Assertion-library agnostic (users bring their own)
- Zero external dependencies
- Parallel execution by default
- No double execution - code runs exactly once per test path
Usage ¶
func TestDatabase(t *testing.T) {
samurai.Run(t, func(s *samurai.Scope) {
var db *DB
s.Test("with database", func(ctx context.Context, w samurai.W) {
db = setupDB(ctx)
w.Cleanup(func() { db.Close() })
}, func(s *samurai.Scope) {
var user *User
s.Test("create user", func(_ context.Context, w samurai.W) {
user = db.CreateUser("test@example.com")
}, func(s *samurai.Scope) {
s.Test("has email", func(_ context.Context, w samurai.W) {
assert.Equal(w.Testing(), "test@example.com", user.Email)
})
s.Test("has name", func(_ context.Context, w samurai.W) {
assert.NotEmpty(w.Testing(), user.Name)
})
})
s.Test("can query all", func(_ context.Context, w samurai.W) {
_, err := db.QueryAll()
assert.NoError(w.Testing(), err)
})
})
})
}
RunWith — Generic Custom Context ¶
RunWith lets you provide a factory that creates a custom context for all callbacks. Embed *BaseContext in your struct to get Testing() and Cleanup() for free:
type MyCtx struct {
*samurai.BaseContext
*assert.Assertions
}
type S = samurai.TestScope[*MyCtx]
func TestDatabase(t *testing.T) {
samurai.RunWith(t, func(w samurai.W) *MyCtx {
return &MyCtx{BaseContext: w, Assertions: assert.New(w.Testing())}
}, func(s *S) {
var count int
s.Test("setup", func(_ context.Context, c *MyCtx) {
count = 42
}, func(s *S) {
s.Test("check", func(_ context.Context, c *MyCtx) {
c.Equal(42, count)
})
})
})
}
Execution Model ¶
Samurai uses a two-phase execution model:
- Discovery — the builder runs to collect test structure (no test code executes)
- Execution — the builder runs fresh per leaf path (test code executes)
This means:
- w.Testing() always returns a valid *testing.T (never nil)
- Each path's code runs exactly once
- Variables declared in builders are isolated per path
Thread Safety ¶
Samurai achieves thread safety through isolation by design:
- Each test path gets its own execution context
- The testing.T instance is per-path (Go's t.Run creates subtests)
- context.Context is shared within a path but immutable
IDE Integration ¶
Paths are emitted as nested t.Run calls mirroring the test tree structure. Each Test name creates a level in the tree. This enables:
- Running individual paths via `go test -run "TestName/Parent/Child"`
- Clicking on specific paths in GoLand/VS Code to run or debug them
- Proper test reporting per path
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run executes a test using the scoped builder API. Variables declared in the builder function are allocated fresh per path, making them parallel-safe without any special framework types.
The only scope method is Test:
s.Test(name, func(context.Context, W)) // leaf test s.Test(name, func(context.Context, W), func(*Scope)) // parent with children
Example:
func TestDatabase(t *testing.T) {
samurai.Run(t, func(s *samurai.Scope) {
var db *DB
s.Test("with database", func(ctx context.Context, w samurai.W) {
db = setupDB(ctx)
w.Cleanup(func() { db.Close() })
}, func(s *samurai.Scope) {
s.Test("can query", func(_ context.Context, w samurai.W) {
_, err := db.QueryAll()
assert.NoError(w.Testing(), err)
})
})
})
}
Example ¶
ExampleRun demonstrates the test tree structure built by Run. Run is called inside a Go test function: func TestXxx(t *testing.T). This example shows path discovery — the first phase of the two-phase model. In real tests, Run handles both discovery and execution automatically.
paths, _ := collectScopedPaths(func(s *Scope) {
s.Test("with database", func(_ context.Context, _ W) {
// setup: create DB, register cleanup
}, func(s *Scope) {
s.Test("can query", func(_ context.Context, _ W) {})
s.Test("can insert", func(_ context.Context, _ W) {})
})
})
for _, p := range paths {
fmt.Println(strings.Join(p.segments, "/"))
}
Output: with database/can query with database/can insert
Example (Nested) ¶
ExampleRun_nested demonstrates deeply nested test paths with multiple branches. Each leaf path executes independently with fresh parent setup.
paths, _ := collectScopedPaths(func(s *Scope) {
s.Test("database", func(_ context.Context, _ W) {}, func(s *Scope) {
s.Test("users", func(_ context.Context, _ W) {}, func(s *Scope) {
s.Test("create", func(_ context.Context, _ W) {})
s.Test("delete", func(_ context.Context, _ W) {})
})
s.Test("posts", func(_ context.Context, _ W) {}, func(s *Scope) {
s.Test("list", func(_ context.Context, _ W) {})
})
})
})
for _, p := range paths {
fmt.Println(strings.Join(p.segments, "/"))
}
Output: database/users/create database/users/delete database/posts/list
Example (Skip) ¶
ExampleRun_skip demonstrates the Skip method. Skip marks all tests in a scope as skipped — their callbacks never execute. Skip propagates to nested scopes. Call order relative to Test does not matter.
paths, _ := collectScopedPaths(func(s *Scope) {
s.Test("stable feature", func(_ context.Context, _ W) {}, func(s *Scope) {
s.Test("works", func(_ context.Context, _ W) {})
})
s.Test("WIP feature", func(_ context.Context, _ W) {}, func(s *Scope) {
s.Skip()
s.Test("not ready", func(_ context.Context, _ W) {})
})
})
for _, p := range paths {
path := strings.Join(p.segments, "/")
if p.skipped {
fmt.Println(path + " (skipped)")
} else {
fmt.Println(path)
}
}
Output: stable feature/works WIP feature/not ready (skipped)
func RunWith ¶
func RunWith[V Context](t *testing.T, factory func(W) V, builder func(*TestScope[V]), opts ...Option)
RunWith executes a test using the scoped builder API with a custom factory. The factory creates a value of type V from *BaseContext for each scope level. This value is passed as the second argument to all Test callbacks.
Use RunWith when you want callbacks to receive a custom type (e.g. an assertion helper). Embed *BaseContext in your custom type to satisfy the Context constraint.
Example:
type MyCtx struct {
*samurai.BaseContext
*assert.Assertions
}
type S = samurai.TestScope[*MyCtx]
func TestDatabase(t *testing.T) {
samurai.RunWith(t, func(w samurai.W) *MyCtx {
return &MyCtx{BaseContext: w, Assertions: assert.New(w.Testing())}
}, func(s *S) {
var count int
s.Test("setup", func(_ context.Context, c *MyCtx) {
count = 42
}, func(s *S) {
s.Test("check", func(_ context.Context, c *MyCtx) {
c.Equal(42, count)
})
})
})
}
Example ¶
ExampleRunWith demonstrates the generic RunWith variant with a custom context. RunWith lets callbacks receive a custom type (e.g. an assertion helper) instead of the default *BaseContext.
type myCtx struct{ *BaseContext }
paths, _ := collectScopedPaths(func(s *TestScope[*myCtx]) {
s.Test("setup", func(_ context.Context, _ *myCtx) {
// factory creates *myCtx from *BaseContext per scope level
}, func(s *TestScope[*myCtx]) {
s.Test("check A", func(_ context.Context, _ *myCtx) {})
s.Test("check B", func(_ context.Context, _ *myCtx) {})
})
})
for _, p := range paths {
fmt.Println(strings.Join(p.segments, "/"))
}
Output: setup/check A setup/check B
Types ¶
type BaseContext ¶
type BaseContext struct {
// contains filtered or unexported fields
}
BaseContext is the framework-provided test context. It implements the Context interface, providing Testing() and Cleanup().
Users can embed *BaseContext in their own struct to create custom test contexts for use with RunWith. The embedded methods (Testing, Cleanup) are promoted, so the custom type satisfies Context automatically.
Example:
type MyCtx struct {
*samurai.BaseContext
*assert.Assertions
}
func (*BaseContext) Cleanup ¶
func (b *BaseContext) Cleanup(fn func())
Cleanup registers a cleanup function that runs after the current scope completes. Multiple cleanups execute in LIFO order (last registered runs first), matching the behavior of Go's t.Cleanup and defer. Cleanups run even if the step panics.
func (*BaseContext) Testing ¶
func (b *BaseContext) Testing() *testing.T
Testing returns the underlying *testing.T for use with any assertion library. Always returns a valid *testing.T (never nil).
type Context ¶
Context is the constraint for custom test context types used with RunWith. Any type satisfying this interface can be used as a test context. The simplest way to satisfy it is to embed *BaseContext.
type Option ¶
type Option func(*runConfig)
Option configures the behavior of Run.
func Parallel ¶
func Parallel() Option
Parallel explicitly enables parallel execution using t.Parallel(). This is the default behavior, so Parallel() is only needed to override a previous Sequential() call or for documentation purposes.
To control the number of parallel tests, use the standard Go test flag:
go test -parallel N ./...
The default parallelism is GOMAXPROCS.
func Sequential ¶
func Sequential() Option
Sequential forces sequential execution. Use this when tests require deterministic execution order. By default, tests run in parallel using t.Parallel().
type Scope ¶
Scope is the default non-generic scope used by Run. It is a type alias for TestScope[W], where W = *BaseContext.
type TestScope ¶
type TestScope[V Context] struct { // contains filtered or unexported fields }
TestScope is the generic scope builder for defining test structure with shared state. The type parameter V determines the test context type passed to all callbacks. Variables declared in the builder function are allocated fresh per path, making them parallel-safe without any special framework types.
Methods:
s.Test(name, fn) — leaf test (no children) s.Test(name, fn, builder) — parent test with children s.Skip() — skip all tests in this scope and descendants
Multiple Test calls per scope are allowed — they become siblings in the test tree.
Example with RunWith:
type MyCtx struct {
*samurai.BaseContext
*assert.Assertions
}
type S = samurai.TestScope[*MyCtx]
samurai.RunWith(t, func(w samurai.W) *MyCtx {
return &MyCtx{BaseContext: w, Assertions: assert.New(w.Testing())}
}, func(s *S) {
var db *DB
s.Test("with database", func(ctx context.Context, w *MyCtx) {
db = setupDB(ctx)
w.Cleanup(func() { db.Close() })
}, func(s *S) {
s.Test("has tables", func(_ context.Context, c *MyCtx) {
c.NotEmpty(db.Tables())
})
})
})
func (*TestScope[V]) Skip ¶
func (s *TestScope[V]) Skip()
Skip marks all tests in this scope as skipped. Skipped tests appear in output as SKIP but their callbacks never execute. The call order relative to Test does not matter — Skip affects the entire scope.
Skip propagates to all nested scopes: if a parent scope is skipped, all descendants are skipped regardless of whether they call Skip themselves.
s.Test("WIP feature", fn, func(s *Scope) {
s.Skip()
s.Test("todo", fn) // skipped
})
func (*TestScope[V]) Test ¶
func (s *TestScope[V]) Test(name string, fn func(context.Context, V), builders ...func(*TestScope[V]))
Test registers a named test in this scope. The name appears in the test tree (go test -v output and IDE).
Without a builder argument, Test creates a leaf test:
s.Test("check value", func(_ context.Context, w W) {
assert.Equal(w.Testing(), expected, actual)
})
With a builder argument, Test creates a parent with children:
s.Test("setup db", func(ctx context.Context, w W) {
db = setupDB(ctx)
w.Cleanup(func() { db.Close() })
}, func(s *Scope) {
s.Test("has tables", func(_ context.Context, w W) { ... })
})
Multiple Test calls per scope are allowed — they become siblings. In discovery mode: records the test without executing fn. In execution mode: records the test; fn is executed later by the execution engine.
type W ¶
type W = *BaseContext
W is the default test context — a pointer to BaseContext. It is used as the context type for the non-generic Run entry point.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
claude-setup
command
Command claude-setup creates a Claude Code skill for the Samurai testing framework.
|
Command claude-setup creates a Claude Code skill for the Samurai testing framework. |