A CLI tool to generate unit test scaffolding for Go projects. It produces closure-based check functions, a before hook for mock setup, and table-driven tests that compose cleanly as test suites grow.
go install github.com/padiazg/go-testgen/cmd/go-testgen@latestOr build from source:
make build
make installgo-testgen gen <pkg-pattern> <FuncSpec> [flags]FuncSpec is either a plain function name (New, CreateUser) or a receiver-qualified method name (Service.CreateUser).
# Constructor
go-testgen gen ./internal/core/services/user New
# Method
go-testgen gen ./internal/core/services/user Service.CreateUser
# Write to stdout instead of file
go-testgen gen ./internal/core/services/user Service.CreateUser -o -
# Write to a specific file
go-testgen gen ./internal/core/services/user Service.CreateUser -o user_test.go
# Also generate a testify mock for an interface used by the function
go-testgen gen ./internal/core/services/user Service.CreateUser \
--mock-from userDomain.UserRepository
# Multiple mocks
go-testgen gen ./internal/core/services/user Service.CreateUser \
--mock-from userDomain.UserRepository \
--mock-from cache.Cache
# Same-package interface (bare name)
go-testgen gen ./internal/transport/i2c I2CTransport.Read \
--mock-from mockI2C
# External interfaces (full import path)
go-testgen gen --mock-from "io.Writer" --pkg mypkg -o -
# Multiple external mocks
go-testgen gen --mock-from "io.Writer" --mock-from "io.Reader" --pkg mypkg --output ./
# Standalone mode (0 args, --mock-from + --pkg + --output required)
go-testgen gen --mock-from "net/http.Handler" --mock-from "io/fs.FS" --pkg mypkg --output ./go-testgen gen ./internal/transport/i2c I2CTransport.Read
--mock-from .I2CTransport
**Flags:**
| Flag | Default | Description |
| - | - | - |
| `-o`, `--output` | auto | Output file. Omit to auto-detect (`<source>_test.go`). Use `-` for stdout. |
| `-v`, `--verbose` | false | Print parsed `FuncInfo` JSON to stderr before generating |
| `--style` | — | Path to `.go-testgen.yaml` config file |
| `--mock-from` | — | Generate a testify mock for an interface (repeatable) — accepts `qualifier.InterfaceName`, `io/fs.FS` (full import path), `.InterfaceName`, or bare `InterfaceName`. In standalone mode (0 positional args), `--pkg` and `--output` are required. |
**Smart merge:** if the target `_test.go` already exists but doesn't contain the test function, go-testgen appends the new test and injects any missing imports. It never overwrites an existing test function without prompting.
### `gen-cases` — materialize test cases from a spec
```bash
go-testgen gen-cases <spec-file> [flags]
Reads a .testspec.yaml describing what to test and inserts the corresponding struct literal entries into the tests slice of the existing _test.go. The scaffolding must already exist (run gen first). Each case includes // ai-hint: comments so a generative AI can complete the concrete before bodies and check values.
# Preview without writing
go-testgen gen-cases --dry-run ./engine/engine_start.testspec.yaml
# Write cases to the test file
go-testgen gen-cases ./engine/engine_start.testspec.yaml
# Override output path
go-testgen gen-cases -o ./engine/engine_test.go ./engine/engine_start.testspec.yaml
# Replace existing entries (re-sync from updated spec)
go-testgen gen-cases --force ./engine/engine_start.testspec.yaml
# Omit ai-hint comments (plain stubs)
go-testgen gen-cases --no-hints ./engine/engine_start.testspec.yamlFlags:
| Flag | Default | Description |
|---|---|---|
--dry-run |
false | Print generated code to stdout without modifying any file. |
-o, --output |
auto | Override the output _test.go path. |
--force |
false | Replace existing entries instead of skipping them. |
--no-hints |
false | Omit // ai-hint: comments from output. |
-v, --verbose |
false | Print a summary of generated/skipped entries. |
Idempotent: running twice produces no duplicates. Use --force to re-sync cases from an updated spec.
Pipeline position: gen → spec authoring → gen-cases → AI fills in before/values → go test.
go-testgen inspect <pkg-pattern> <FuncSpec>Prints the FuncInfo JSON that gen uses internally. Useful for diagnosing wrong signatures or missing types.
go-testgen report <pkg-pattern> [--format text|table|json]Scans all exported functions and methods in a package and shows:
- Whether a test function exists
- Interface dependencies inferred from struct fields
- Mock file existence
- Exact
go-testgen gencommand to run for each untested function
Example output (text format):
Package: github.com/padiazg/user-manager/internal/core/services/user
Source: /path/to/internal/core/services/user
✓ TestService_New
New(cfg *Config) *Service
✗ TestService_CreateUser
Service.CreateUser(ctx context.Context, req *userDomain.UserCreateRequest) (*userDomain.User, error)
Interface deps:
userDomain.UserRepository mock_userrepository_test.go ✓
Suggest: go-testgen gen ./internal/core/services/user Service.CreateUser
✗ TestService_FindByID
Service.FindByID(ctx context.Context, id string) (*userDomain.User, error)
Interface deps:
userDomain.UserRepository mock_userrepository_test.go ✓
Suggest: go-testgen gen ./internal/core/services/user Service.FindByID--mock-from flags are only suggested for interfaces whose mock file does not yet exist.
Formats:
| Flag | Description |
|---|---|
--format text |
Default. Human-readable with ✓/✗ |
--format table |
Tabular layout (uses go-pretty) |
--format json |
Machine-readable JSON |
type checkServiceCreateUserFn func(*testing.T, *userDomain.User, error)
var checkServiceCreateUser = func(fns ...checkServiceCreateUserFn) []checkServiceCreateUserFn {
return fns
}
func checkCreateUserError(want string) checkServiceCreateUserFn {
return func(t *testing.T, _ *userDomain.User, err error) {
t.Helper()
if want == "" {
assert.NoErrorf(t, err, "checkCreateUserError: expected no error, got %v", err)
return
}
if assert.Errorf(t, err, "checkCreateUserError: expected error %q", want) {
assert.Containsf(t, err.Error(), want, "checkCreateUserError mismatch")
}
}
}
func TestService_CreateUser(t *testing.T) {
tests := []struct {
name string
req *userDomain.UserCreateRequest
before func(*Service)
checks []checkServiceCreateUserFn
}{
{
name: "TODO: success case",
checks: checkServiceCreateUser(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
s := New(nil)
if tt.before != nil {
tt.before(s)
}
r, err := s.CreateUser(context.Background(), tt.req)
for _, c := range tt.checks {
c(t, r, err)
}
})
}
}Key properties:
- Check functions are closures — each assertion is a separate
checkXxxFn, composable viacheckXxx(fn1, fn2, ...). - The
beforehook sets up mock expectations per test case. - The check function signature mirrors the function's full return list (including
error). - Context parameters are injected automatically (
context.Background()), not exposed in the table.
--mock-from accepts four formats: qualifier.InterfaceName (cross-package via consuming imports), io/fs.FS (full import path for external/stdlib), .InterfaceName (same package with dot), or bare InterfaceName (same package). Generates a complete testify mock written to mock_<interfacename>_test.go in the same directory as the test file. In standalone mode (0 positional args), --pkg and --output are required.
type mockUserRepository struct {
mock.Mock
}
func (m *mockUserRepository) CreateUser(ctx context.Context, req *userDomain.UserCreateRequest) (*userDomain.User, error) {
args := m.Called(ctx, req)
r, _ := args.Get(0).(*userDomain.User)
return r, args.Error(1)
}
func (m *mockUserRepository) FindByID(ctx context.Context, id string) (*userDomain.User, error) {
args := m.Called(ctx, id)
r, _ := args.Get(0).(*userDomain.User)
return r, args.Error(1)
}- All interface methods are implemented.
- Pointer/slice returns use comma-ok type assertion (
r, _ := args.Get(0).(*T)) — safe when mock returnsnil. - Existing mock files are never overwritten.
Create .go-testgen.yaml in your project root:
receiver_var_name: "s" # variable name for the receiver in tests
result_var_name: "r" # variable name for non-error return values
error_var_name: "err" # variable name for the error return
use_testify: true # use testify assert helpers
add_todo_cases: true # add placeholder TODO test cases
number_of_todos: 1 # how many TODO cases to add
check_type_suffix: "Fn" # suffix for check type names (e.g. checkCreateUserFn)
generate_mocks: true # generate mock files when --mock-from is used
generate_checks: true # generate checkXxx helper functionsSkills for AI coding agents (OpenCode, Claude Code, Cursor, Codex, Gemini) that guide them in using go-testgen's closure-check test pattern and generating comprehensive test cases.
# Via curl (recomendado, sin clone)
curl -fsSL https://raw.githubusercontent.com/padiazg/go-testgen/main/scripts/install.sh | bash
# Con target personalizado
curl -fsSL ... | bash -s -- /path/to/target/skills
# Local (con repo clonado)
./scripts/install.shInstalls closure-check-tests and gen-test-cases skills, plus AGENTS.md
as a pipeline reference under the go-testgen/ subdirectory to avoid
collisions with other agents' configuration.
After installation, instruct your AI agent to read both skills before generating test cases:
Read skills/closure-check-tests/SKILL.md and skills/gen-test-cases/SKILL.md.
Then read [source file] and [existing _test.go].
Generate test cases for TestXxx_Yyy.
go-testgen detects and correctly handles all channel types — chan, chan<-, <-chan — in both parameters and results.
# Function returning a receive-only channel
go-testgen gen ./internal/pkg ChannelRecvReturn
# Function accepting a send-only channel parameter
go-testgen gen ./internal/pkg ChannelSendParam
# Bidirectional channel
go-testgen gen ./internal/pkg ChannelBidiThe analyzer extracts channel direction (ChanDir) and generates correct placeholder values (nil) for channel types.
# 1. See what needs tests
go-testgen report ./internal/core/services/user
# 2. Generate tests + mocks for each untested function
go-testgen gen ./internal/core/services/user Service.CreateUser \
--mock-from userDomain.UserRepository
# 3. Author a .testspec.yaml describing the scenarios (manually or via AI)
# Then materialize the cases into the test file
go-testgen gen-cases ./internal/core/services/user/service_create_user.testspec.yaml
# 4. (Optional) Ask an AI to fill in the ai-hint: stubs using the spec + source
# 5. Run tests
go test ./internal/core/services/user/...