testutil

package
v1.148.1 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package testutil solves repetitive test setup problems that appear across Go codebases: forcing I/O failures on demand, capturing process output, bootstrapping HTTP handlers, and normalizing time-variant values in assertions.

Problem

Unit and integration tests often need infrastructure behavior that is hard to trigger deterministically in production code: `io.Reader` errors, `io.Closer` errors, router wiring for request/response assertions, and snapshot comparisons for payloads that contain changing timestamps. Reimplementing those helpers in every package leads to inconsistent tests and duplicated boilerplate.

testutil centralizes these patterns into small, focused helpers designed for test-only usage.

Key Features

  • Deterministic I/O failure mocks: NewErrorReader returns a reader whose ErrorReader.Read always fails; NewErrorCloser returns an io.ReadCloser whose ErrorCloser.Close always fails. These are useful for exercising error paths that are otherwise difficult to trigger.
  • HTTP test bootstrap: RouterWithHandler builds an http.Handler backed by `julienschmidt/httprouter` with a route pre-registered, reducing per-test router setup noise.
  • Output capture for assertions: CaptureOutput redirects stdout, stderr, and the default logger for the duration of a function call and returns captured output as a string, enabling stable assertions for CLI/log-producing code.
  • Time-variant text normalization: ReplaceDateTime and ReplaceUnixTimestamp replace dynamic timestamp fragments in strings (for example JSON responses), making golden/snapshot assertions deterministic.

Benefits

  • Less test boilerplate and more consistent test patterns across packages.
  • Easier coverage of failure branches and edge cases.
  • More stable, less flaky string-based assertions.

Usage

reader := testutil.NewErrorReader("read failed")
closer := testutil.NewErrorCloser("close failed")
_ = reader
_ = closer

output := testutil.CaptureOutput(t, func() {
    fmt.Println("hello")
})

h := testutil.RouterWithHandler(http.MethodGet, "/health", func(w http.ResponseWriter, _ *http.Request) {
    w.WriteHeader(http.StatusOK)
})
_ = h

normalized := testutil.ReplaceDateTime(responseBody, "<DATETIME>")
normalized = testutil.ReplaceUnixTimestamp(normalized, "<UNIX_TS>")
_ = output
_ = normalized

These functions are intended for use in tests only.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CaptureOutput

func CaptureOutput(t *testing.T, fn func()) string

CaptureOutput captures stdout, stderr, and standard-logger output produced while fn runs, and returns it as a single string.

It temporarily redirects the process-global os.Stdout, os.Stderr, and the standard logger (via log.SetOutput) to an in-memory pipe, runs fn, then restores all three — including the logger's previous output destination — before returning. Restoration and cleanup happen on every exit path, including a panic inside fn.

Because it mutates process-global state, CaptureOutput MUST NOT be used from parallel tests (t.Parallel) or while other goroutines write to stdout, stderr, or the standard logger; concurrent use would capture interleaved or missing output.

func ReplaceDateTime

func ReplaceDateTime(src, repl string) string

ReplaceDateTime replaces RFC3339-like datetime substrings in src with repl. It is useful for deterministic assertions on JSON responses with dynamic timestamps.

The match is bounded to the RFC3339 grammar (date, "T", time, optional fractional seconds and timezone offset) and never spans newlines. Space-separated datetimes (for example "2006-01-02 15:04:05") are intentionally not matched.

func ReplaceUnixTimestamp

func ReplaceUnixTimestamp(src, repl string) string

ReplaceUnixTimestamp replaces standalone 19-digit integers in src with repl. It is useful for deterministic assertions on JSON responses with dynamic Unix-nanosecond timestamps.

This is a heuristic keyed only on digit count: any standalone 19-digit integer is replaced, including unrelated identifiers that happen to be exactly 19 digits long. Numbers with more or fewer than 19 digits are left untouched.

func RouterWithHandler

func RouterWithHandler(method, path string, handlerFunc http.HandlerFunc) http.Handler

RouterWithHandler returns a new httprouter with handlerFunc registered for method and path.

Types

type ErrorCloser

type ErrorCloser struct {
	io.Reader
	// contains filtered or unexported fields
}

ErrorCloser is an io.ReadCloser whose Close method always returns an error. Its Read side yields io.EOF immediately (an empty body); only Close fails. The embedded reader is an io.Reader (not a concrete bytes.Reader), so the exported surface is exactly io.ReadCloser.

func NewErrorCloser

func NewErrorCloser(errMsg string) *ErrorCloser

NewErrorCloser creates an ErrorCloser that returns errMsg from Close.

func (*ErrorCloser) Close

func (e *ErrorCloser) Close() error

Close always returns the configured error.

type ErrorReader

type ErrorReader struct {
	// contains filtered or unexported fields
}

ErrorReader is an io.Reader that always returns an error.

func NewErrorReader

func NewErrorReader(msg string) *ErrorReader

NewErrorReader creates an ErrorReader that always returns msg as an error.

func (*ErrorReader) Read

func (r *ErrorReader) Read([]byte) (int, error)

Read always returns the configured error.

type TestHTTPResponseWriter

type TestHTTPResponseWriter interface {
	http.ResponseWriter
}

TestHTTPResponseWriter wraps http.ResponseWriter to simplify mock generation.

Jump to

Keyboard shortcuts

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