Skip to content

padiazg/go-testgen

Repository files navigation

go-testgen

Go Reference Go Report Card License: MIT

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.

Installation

go install github.com/padiazg/go-testgen/cmd/go-testgen@latest

Or build from source:

make build
make install

Commands

gen — generate test scaffolding

go-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.yaml

Flags:

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.

inspect — debug parsed function info

go-testgen inspect <pkg-pattern> <FuncSpec>

Prints the FuncInfo JSON that gen uses internally. Useful for diagnosing wrong signatures or missing types.

report — package test coverage overview

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 gen command 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

Generated test style

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 via checkXxx(fn1, fn2, ...).
  • The before hook 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.

Generated mock style

--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 returns nil.
  • Existing mock files are never overwritten.

Configuration

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 functions

AI Agent Skills

Skills 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.

Install

# 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.sh

Installs 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.

Usage

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.

Channel Type Support

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 ChannelBidi

The analyzer extracts channel direction (ChanDir) and generates correct placeholder values (nil) for channel types.

Typical workflow

# 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/...

About

A test scaffolding tool for Golang

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages