go-testgen

command module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: MIT Imports: 1 Imported by: 0

README

go-testgen

go-testgen is a CLI tool that generates 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

# Same-package interface (dot prefix)
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, .InterfaceName, or bare InterfaceName

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.

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 three formats: qualifier.InterfaceName (external import), .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.

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

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. Subsequent functions reuse the existing mock (no --mock-from needed)
go-testgen gen ./internal/core/services/user Service.FindByID

# 4. Fill in test cases, run tests
go test ./internal/core/services/user/...

Documentation

Overview

Copyright © 2026 Patricio Diaz <padiazg@gmail.com>

Directories

Path Synopsis
internal
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL