tdd

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package tdd is a generics-based table-driven test library for Forge projects. It collapses the per-RPC test boilerplate (table struct, range loop, error-code assertion, mock construction) into a small set of reusable helpers so that scaffolded test files can be tiny shims that declare cases and let the library carry the iteration.

The library is dependency-light on purpose — it uses only the standard library plus connectrpc.com/connect (the same RPC dependency Forge projects already pull in). It does not introduce assertion frameworks, mock generators, or other runtime dependencies.

The four helpers

  • TableRPC runs a slice of Case rows against a Connect handler function and asserts on either a happy-path response or an expected connect.Code error.

  • TableContract runs a slice of ContractCase rows against a contract.go-defined Service interface implementation. Each case supplies a closure that invokes one method; the helper compares the returned error and (optionally) the returned value.

  • E2EClient takes an *httptest.Server and a typed-client factory and returns a client wired to the server's URL, registering t.Cleanup(srv.Close) for you.

  • NewMock is a tiny option-based constructor for the Func-field mocks Forge generates from contract.go (mock_gen.go).

Standalone helpers

  • AssertConnectError asserts that an error is a Connect error with the expected code, with a clear message on mismatch.

  • WithTimeout returns a context.Context with the given deadline and a cleanup func, suitable for table-row Setup hooks.

  • SetupMockDB returns a real-postgres *sql.DB (pkg/pgtest), registers cleanup, and is the same shape used by the bootstrap_testing scaffold.

See pkg/tdd/*_test.go for usage examples; the same patterns appear in the templates under internal/templates/{service,internal-package,test/e2e}.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertConnectError

func AssertConnectError(t *testing.T, err error, want connect.Code)

AssertConnectError asserts that err is a non-nil Connect error with the given code. It is the canonical assertion helper used inside TableRPC and is exported for direct use in hand-written tests.

func E2EClient

func E2EClient[Client any](t *testing.T, srv *httptest.Server, newClient func(url string) Client) Client

E2EClient takes an httptest.Server and a typed-client factory and returns a client wired to the server's URL. Cleanup of the server is registered via t.Cleanup, so the caller does not need to close it.

Typical usage with a generated Connect client:

client := tdd.E2EClient(t, srv, func(url string) myv1connect.MyServiceClient {
    return myv1connect.NewMyServiceClient(http.DefaultClient, url)
})

The factory receives the live server URL (including scheme and port).

func NewMock

func NewMock[T any](opts ...MockOption[T]) *T

NewMock constructs a zero-valued T and applies each option.

T is typically a Forge-generated MockService. The returned pointer is safe to assign into a Deps struct that expects the contract interface — Forge mocks have a *MockService method-set that satisfies their matching Service interface.

func RunRPCCases

func RunRPCCases[Req, Resp any](
	t *testing.T,
	cases []RPCCase[Req, Resp],
	handler HandlerFunc[Req, Resp],
)

RunRPCCases is a function alias for TableRPC — see RPCCase. The codegen-emitted test shim uses RunRPCCases so the generated call site mirrors the migration skill's documentation. Hand-written tests may call either; they have identical behaviour.

func SetupMockDB

func SetupMockDB(t *testing.T) *sql.DB

SetupMockDB returns a fresh, isolated real-postgres *sql.DB suitable for hermetic unit tests. The DB and its underlying database are cleaned up via t.Cleanup, so the caller does not need to close it manually.

forge is postgres-pinned: the DB is a per-test database on the process-shared ephemeral postgres (pkg/pgtest — embedded-postgres by default, or the FORGE_TEST_POSTGRES_URL server). No driver blank-import is required; pgtest owns the driver. The first call in a process boots the shared server (downloading the pg binary on a fresh machine).

func TableContract

func TableContract[T any](t *testing.T, impl T, cases []ContractCase)

TableContract runs a slice of ContractCase rows. Each row becomes a t.Run subtest. The helper invokes Setup, runs Call, and:

  • if WantErr is set, asserts errors.Is(err, WantErr),
  • else if Check is set, runs it on the returned value,
  • else compares the returned value to Want via reflect.DeepEqual.

The impl parameter is unused at runtime — it exists so the call site reads naturally (TableContract(t, svc, cases)) and so the Go type system carries the implementation type into the closure capture, which is the most ergonomic shape we found for an "any method on T" table.

func TableRPC

func TableRPC[Req, Resp any](
	t *testing.T,
	cases []Case[Req, Resp],
	handler HandlerFunc[Req, Resp],
)

TableRPC runs a slice of Case rows against a Connect handler. Each row becomes a t.Run subtest. The helper:

  • calls Setup if present,
  • invokes the handler with the row's request,
  • asserts on WantErr (if set) or runs Check (if the call succeeded).

Subtests run with t.Parallel disabled by default so per-row Setup that touches shared state stays correct. Wrap the call site in a parallel outer test if you want concurrency; per-row parallelism is the caller's choice and not the library's default.

func WithTimeout

func WithTimeout(d time.Duration) (context.Context, context.CancelFunc)

WithTimeout returns a context.Context with the given deadline and a cleanup function that cancels it. Suitable for use inside a Case.Setup hook or directly as Case.Ctx.

tdd.Case[Req, Resp]{
    Name: "slow", Req: ...,
    Ctx:  func() context.Context { ctx, _ := tdd.WithTimeout(2*time.Second); return ctx }(),
},

Cancel is exposed so tests that want eager cleanup can defer it; the returned context will already be canceled by Go's runtime once the timeout elapses.

Types

type Case

type Case[Req, Resp any] struct {
	// Name identifies the row; passed straight to t.Run.
	Name string

	// Req is the inbound *connect.Request used in the call.
	Req *connect.Request[Req]

	// WantErr, if non-zero, asserts the handler returned a connect.Error
	// with this code. The Check function is not consulted in this case.
	WantErr connect.Code

	// Check is invoked on a successful (err == nil) response so the test
	// can assert on the returned message. Optional.
	Check func(t *testing.T, resp *connect.Response[Resp])

	// Setup runs before the handler is invoked. Use it to seed mocks or
	// populate per-row state. Cleanup should be registered via t.Cleanup
	// inside the closure.
	Setup func(t *testing.T)

	// Ctx, if non-nil, overrides the default context.Background() passed
	// to the handler. Use [WithTimeout] for a deadlined context.
	Ctx context.Context
}

Case is a table-driven test row for a Connect RPC.

Req is the request proto type and Resp is the response proto type; Forge handlers receive *connect.Request[Req] and return *connect.Response[Resp], which matches the signature the helper expects.

Either WantErr or Check should be set — never both. WantErr asserts the handler returned a Connect error with the given code; Check is a caller-supplied function for asserting on the response (typically used for happy-path cases). If neither is set, the helper only verifies that the call did not return an error.

There is deliberately no "tolerate any outcome" mode: every row must be able to fail. Scaffold rows for not-yet-implemented handlers assert WantErr: connect.CodeUnimplemented — such a row self-destructs (goes red) the moment the handler is implemented, forcing it to be rewritten with a real Check / WantErr assertion.

type ContractCase

type ContractCase struct {
	// Name identifies the row; passed straight to t.Run.
	Name string

	// Call invokes one method on the contract implementation. The
	// returned value is compared against Want using reflect.DeepEqual
	// when WantErr is nil.
	Call func() (any, error)

	// Want is the expected return value (compared with reflect.DeepEqual).
	// Ignored when WantErr is set or Check is non-nil.
	Want any

	// WantErr, if non-nil, asserts that Call returned an error matching
	// it via errors.Is. Use a sentinel error or a wrapped error.
	WantErr error

	// Check is an alternative to Want — it runs on the returned value
	// after a successful call and lets the test assert custom predicates.
	Check func(t *testing.T, got any)

	// Setup runs before Call. Use it to wire mocks or seed state for
	// this row. Cleanup should be registered via t.Cleanup.
	Setup func(t *testing.T)
}

ContractCase is a table-driven test row for a single contract-method invocation. Each row supplies a Call closure that invokes one method on the contract implementation. The closure returns (any, error) so a single ContractCase type can drive any method shape — multi-return methods adapt by packing into a struct, single-return methods return the value directly.

type HandlerFunc

type HandlerFunc[Req, Resp any] func(context.Context, *connect.Request[Req]) (*connect.Response[Resp], error)

HandlerFunc is the Connect RPC handler signature TableRPC drives.

type MockOption

type MockOption[T any] func(*T)

MockOption configures a Forge-generated Func-field mock.

Forge's mock_gen.go produces structs of the form

type MockService struct {
    DoXFunc func(...) (...)
    DoYFunc func(...) (...)
}

MockOption[T] is a functional option that mutates such a struct. Use it with NewMock to build a mock at the call site:

m := tdd.NewMock(
    func(m *email.MockService) { m.SendFunc = func(...) error { return nil } },
)

MockOption is a plain function alias so callers can write inline closures without naming the option type, which keeps test files short.

type RPCCase

type RPCCase[Req, Resp any] = Case[Req, Resp]

RPCCase is a type alias for Case — used by the codegen-emitted `handlers_crud_test_gen.go` shim so the generated identifier matches the migration skill's documentation. Hand-written tests may use either name; they are the same type.

Jump to

Keyboard shortcuts

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