goldkit

module
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT

README

Go Go Reference

goldkit

Golden-file testing for Go — readable YAML fixtures, per-test templating, and a whole HTTP request/response exchange asserted from a single file.

Overview

goldkit compares values under test against golden files: expected outputs kept beside your tests. They are structured, commentable YAML — not opaque blobs — so a reviewer sees exactly what changed in a diff, and a test can render the fixture as a Go template when it needs per-case data.

Its standout capability is HTTP: a single golden file can describe a whole exchange — the request, the expected response, even multipart bodies — and goldkit replays the request against a server and asserts the response for you. It builds on ctx42/testing for assertions and pairs with ctx42/testkit's httpkit for driving test servers.

Features

  • One-file HTTP exchanges — define a request and its expected response (multipart included) in one YAML file and assert the round trip.
  • Readable YAML fixtures — structured, commentable golden files that review cleanly in a pull request.
  • Templating — render a fixture with per-test data via Go text/template; delimiters are configurable.
  • Semantic JSON equality — JSON bodies match on data, not byte layout, so formatting and key order don't break a test.
  • Multipart bodies — build and assert multipart/form-data with a fixed boundary, or get a ready-to-send *http.Request.
  • Typed metadata — attach free-form meta to a fixture and read it back with typed, error-returning getters.

Installation

go get github.com/ctx42/goldkit
import "github.com/ctx42/goldkit/pkg/goldkit"

Quickstart

Keep the expected output in a golden file next to your test:

# testdata/golden.yml
bodyType: text
body: |
  Hello, Alice!

Assert a value against it:

func TestGreeting(t *testing.T) {
	gld := goldkit.Create(t, "testdata/golden.yml", nil)
	gld.Assert([]byte("Hello, Alice!\n"))
}

[!NOTE] Create and Assert take your test's *testing.T, so that flow lives in a test function. The fenced examples further down are runnable Go Example functions kept in sync with the package tests.

Usage

Text and JSON bodies

A golden file declares a bodyType. Text bodies compare byte-for-byte; JSON bodies compare by semantic equality, so indentation and key order don't matter.

# testdata/base_json.yml
bodyType: json
body: |-
  {"key2": "val2"}

Against that fixture, both of these pass:

gld := goldkit.Create(t, "testdata/base_json.yml", nil)
gld.Assert([]byte(`{"key2":"val2"}`))              // compact
gld.Assert([]byte("{\n  \"key2\": \"val2\"\n}"))   // pretty — same data
Templating

Pass a data object and the golden file is rendered as a Go template before use, so one fixture serves many cases:

# testdata/eg_greeting.tpl.yml
body: |
  Hello, {{ .Name }}!
m := map[string]string{"Name": "alice"}
src, err := goldkit.SourceFrom("testdata/eg_greeting.tpl.yml", m)
if err != nil {
	panic(err)
}

body, _ := io.ReadAll(src)
fmt.Print(string(body))
// Output:
// body: |
//   Hello, alice!
Custom delimiters

When the golden content itself contains {{/}}, switch the template delimiters with Delims:

data := map[string]string{"key1": "val1"}
src, err := goldkit.SourceFrom(
	"testdata/golden_custom_delim.tpl.yml",
	data,
	goldkit.Delims("[[", "]]"),
)
if err != nil {
	panic(err)
}

body, _ := io.ReadAll(src)
fmt.Print(string(body))
// Output:
// meta:
//   key1: val1
Typed metadata

A golden file — or any Meta map — carries free-form key/values you read back with typed, error-returning getters:

m := goldkit.Meta{"user": "alice", "attempts": 3}

user, _ := m.MetaGetString("user")
attempts, _ := m.MetaGetInt("attempts")

fmt.Printf("%s made %d attempts\n", user, attempts)
// Output:
// alice made 3 attempts

Richer scalar types are parsed and validated too — RFC 3339 timestamps, floats, ints, durations, and locations:

m := goldkit.Meta{"when": "2000-01-02T03:04:05Z", "ratio": 12.5}

when, _ := m.MetaGetTime("when")
ratio, _ := m.MetaGetFloat64("ratio")

fmt.Println(when.Format("2006-01-02"))
fmt.Println(ratio)
// Output:
// 2000-01-02
// 12.5
Multipart bodies

Build a multipart/form-data body with a fixed boundary:

mp := goldkit.NewMultipart()
if err := mp.SetBoundary("example-boundary"); err != nil {
	panic(err)
}
if err := mp.AddField("greeting", "hello"); err != nil {
	panic(err)
}

h := make(http.Header)
mp.SetContentTypeHeader(h)

fmt.Println(h.Get("Content-Type"))
// Print the wire body one line at a time so the CRLF framing is visible.
for _, line := range bytes.Split(mp.Body(), []byte("\r\n")) {
	fmt.Printf("%q\n", line)
}
// Output:
// multipart/form-data; boundary=example-boundary
// "--example-boundary"
// "Content-Disposition: form-data; name=\"greeting\""
// ""
// "hello"
// "--example-boundary--"
// ""

Or get a ready-to-send *http.Request with the body and Content-Type already set:

mp := goldkit.NewMultipart()
if err := mp.SetBoundary("example-boundary"); err != nil {
	panic(err)
}
if err := mp.AddField("greeting", "hello"); err != nil {
	panic(err)
}

req, err := mp.Request(http.MethodPost, "/upload")
if err != nil {
	panic(err)
}

fmt.Println(req.Method, req.URL.Path)
fmt.Println(req.Header.Get("Content-Type"))
// Output:
// POST /upload
// multipart/form-data; boundary=example-boundary
HTTP requests and responses

Describe a request or response entirely in YAML, then assert an actual *http.Request or *http.Response against it:

# testdata/request_full.yml
request:
  method: POST
  path: /some/path
  query: key0=val0&key1=val1
  headers:
    - 'Authorization: Bearer token'
  bodyType: text
  body: |
    abc
func TestOutboundRequest(t *testing.T) {
	_, src := goldkit.Open(t, "testdata/request_full.yml", nil)
	want := goldkit.NewRequest(t, src)

	got := buildRequest() // the *http.Request your code produces
	want.Assert(got)
}
Full HTTP exchange

The headline: one file holds both the request and the expected response. goldkit replays the request against a server and asserts the response in a single call.

# testdata/exchange.yml (abbreviated)
request:
  scheme: http
  host: {{ .host }}
  method: POST
  path: /some/path
  query: key0=val0&key1=val1
  headers:
    - 'Authorization: Bearer token'
  bodyType: json
  body: |
    {"key2": "val2"}
response:
  statusCode: 200
  headers:
    - 'Content-Type: application/json'
  bodyType: json
  body: |
    {"success": true}
func TestExchange(t *testing.T) {
	// A recording test server that returns the canned response.
	srv := httpkit.NewServer(t)
	srv.Rsp(http.StatusOK, []byte(`{"success": true}`)).
		Header("Content-Type", "application/json")

	// The exchange file targets that server via its templated host.
	u, _ := url.Parse(srv.URL())
	data := goldkit.Meta{}.MetaSet("host", u.Host)
	src, _ := goldkit.SourceFrom("testdata/exchange.yml", data)

	// Replay the request and assert the response in one call.
	req, res := goldkit.NewExchange(t, src).Assert()
	_, _ = req, res
}

[!NOTE] This example drives a live test server on a random port, so it is illustrative rather than a deterministic Go Example. The exchange file's host is templated ({{ .host }}) and filled in per run.

Golden file format

A value-under-test golden file:

Key Meaning
bodyType text (default), json, multipart, or none.
body Expected body; rendered as a template when data is passed.
meta Free-form key/values, read via the typed Meta getters.

HTTP golden files wrap fields under request: and/or response:method, path, query, headers, statusCode, plus the same bodyType, body, and meta keys. A file with both a request: and a response: is an exchange.

Resources

Directories

Path Synopsis
pkg
goldkit
Package goldkit provides structures helping to test with golden files defined as YAML.
Package goldkit provides structures helping to test with golden files defined as YAML.

Jump to

Keyboard shortcuts

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