Documentation
¶
Overview ¶
Package gotestify extends github.com/stretchr/testify with assertions on JSON documents and tabular output. Its helpers take the same TestingT that testify's assert package accepts, follow the same (t, expected, actual) argument order, and return a bool the same way, so they sit naturally next to assert and require in a test.
JSONEqual and JSONSubset compare two JSON documents and fail the test with a colorized diff. The expected side may contain {{...}} placeholders that match a value by shape ({{uuid}}, {{timestamp}}, {{any}}, or anything added with RegisterMatcher), capture a value for reuse, or interpolate previously captured values; see Compare for the details.
PrintTable and TableEqual render a slice of values as a text table keyed by JSON field name and diff it cell by cell.
Index ¶
- func CaptureSlog(t CleanupT) *bytes.Buffer
- func CollectVars(a, b string, opts *DiffOptions) (map[string]any, error)
- func CollectVarsStream(a, b io.Reader, opts *DiffOptions) (map[string]any, error)
- func JSONEqual(t TestingT, expected, actual any, jsonOpts ...json.Options) bool
- func JSONSubset(t TestingT, expected, actual any, jsonOpts ...json.Options) bool
- func PrettyJSON(val any, opts ...json.Options) string
- func PrintTable(data any, columns []string, opts ...json.Options) (string, error)
- func RegisterMatcher(name string, fn Matcher)
- func TableEqual(t TestingT, expected string, data any, columns []string, ...) bool
- type CleanupT
- type DiffOptions
- type Difference
- type Matcher
- type Number
- type Tag
- type TestingT
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CaptureSlog ¶
CaptureSlog routes the default slog logger into the returned buffer for the rest of the test, restoring the previous logger at cleanup. Not safe under t.Parallel: the default logger is process-global.
Example ¶
var t reporter
buf := gotestify.CaptureSlog(&t)
slog.Info("work completed", "items", 3)
fmt.Println(contains(buf.String(), "work completed"), contains(buf.String(), "items=3"))
for _, f := range t.cleanups {
f()
}
Output: true true
func CollectVars ¶
func CollectVars(a, b string, opts *DiffOptions) (map[string]any, error)
CollectVars collects the variables from two JSON documents using given options.
Example ¶
vars, err := gotestify.CollectVars(
`{"id": "{{uid}}", "team": {"id": "{{tid}}"}}`,
`{"id": "u_1", "team": {"id": "t_9"}}`,
gotestify.JSONDiffOptions(),
)
fmt.Println(err, vars["uid"], vars["tid"])
Output: <nil> u_1 t_9
func CollectVarsStream ¶
CollectVarsStream collects the variables from two JSON documents streamed by the specified readers using given options.
func JSONEqual ¶
JSONEqual reports whether expected and actual are the same JSON document, failing t with a colorized diff when they are not. Either argument may be a JSON string or any value marshalable with encoding/json/v2 and jsonOpts. The expected side may use {{...}} placeholders: see Compare.
Example ¶
var t reporter
user := getUser()
ok := gotestify.JSONEqual(&t, `{
"id": "{{uuid}}",
"email": "alice@example.com",
"team_id": "{{uuid}}",
"created_at": "{{timestamp}}"
}`, user)
fmt.Println(ok)
Output: true
Example (CaptureVariables) ¶
var t reporter
resp := `{
"user": {"id": "u_1", "name": "Alice"},
"owner": {"id": "u_1"},
"self": "/users/u_1"
}`
// {{uid}} binds to the first value it meets and must match everywhere it
// recurs; "/users/{{uid}}" is rendered from the captured value.
ok := gotestify.JSONEqual(&t, `{
"user": {"id": "{{uid}}", "name": "Alice"},
"owner": {"id": "{{uid}}"},
"self": "/users/{{uid}}"
}`, resp)
fmt.Println(ok)
Output: true
func JSONSubset ¶
JSONSubset is JSONEqual that also accepts extra properties on the actual side, for asserting on part of a larger payload.
Example ¶
var t reporter
resp := `{"id": "0193f2a1-4c3b-7a91-b8e2-1f5d9c7a3e40", "name": "Alice", "internal": true}`
// Extra properties on the actual side are fine.
ok := gotestify.JSONSubset(&t, `{"id": "{{uuid}}", "name": "Alice"}`, resp)
fmt.Println(ok)
Output: true
func PrettyJSON ¶
PrettyJSON formats the given value as an indented JSON string. On failure it returns the error text, so it stays usable inside a failure message.
func PrintTable ¶
PrintTable renders data as a text table with the given columns. Columns are JSON field names: see createTable.
func RegisterMatcher ¶
RegisterMatcher makes {{name}} usable as a placeholder on the expected side of a comparison, matching any value for which fn returns true. Registering a name again replaces the previous matcher. It panics if name is empty or fn is nil.
Example ¶
var t reporter
gotestify.RegisterMatcher("email", func(v any) bool {
s, ok := v.(string)
return ok && len(s) > 3 && s[0] != '@' && s[len(s)-1] != '@' && contains(s, "@")
})
ok := gotestify.JSONEqual(&t, `{"email": "{{email}}"}`, `{"email": "alice@example.com"}`)
fmt.Println(ok)
Output: true
func TableEqual ¶
func TableEqual(t TestingT, expected string, data any, columns []string, jsonOpts ...json.Options) bool
TableEqual renders data as a table (see PrintTable) and reports whether it matches expected line by line and cell by cell, failing t with a colorized diff when it does not. Leading and trailing whitespace on each expected line is ignored, as are blank lines, so the literal can be indented to match the surrounding code.
Example ¶
var t reporter
type Row struct {
Name string `json:"name"`
Age int `json:"age"`
}
rows := []Row{{"Alice", 25}, {"Bob", 30}}
// Columns are JSON field names; whitespace around each expected line is
// ignored so the literal can be indented.
ok := gotestify.TableEqual(&t, `
Alice |25 |
Bob |30 |
`, rows, []string{"name", "age"})
fmt.Println(ok)
Output: true
Types ¶
type CleanupT ¶
type CleanupT interface {
Cleanup(func())
}
CleanupT is the subset of *testing.T the capture needs.
type DiffOptions ¶
type DiffOptions struct {
Normal Tag
Added Tag
Removed Tag
Changed Tag
Skipped Tag
Prefix string
Indent string
PrintTypes bool
ChangedSeparator string
// When provided, this function will be used to compare two numbers. By default numbers are compared using their
// literal representation byte by byte.
CompareNumbers func(a, b Number) bool
// When true, only differences will be printed. By default, it will print the full json.
SkipMatches bool
// contains filtered or unexported fields
}
Options controls how Compare renders a diff. It is unrelated to json.Options.
func ConsoleDiffOptions ¶
func ConsoleDiffOptions() *DiffOptions
ConsoleDiffOptions provides a set of options that are well suited for console output. Options use ANSI foreground color escape sequences to highlight changes. It returns the default console options.
func JSONDiffOptions ¶
func JSONDiffOptions() *DiffOptions
JSONDiffOptions provides a set of options in JSON format that are fully parseable. It returns the default JSON options.
type Difference ¶
type Difference int
Difference is the difference type.
const ( // FullMatch means provided arguments are deeply equal. FullMatch Difference = iota // SupersetMatch means first argument is a superset of a second argument. SupersetMatch // SubsetMatch means first argument is a subset of a second argument. SubsetMatch // NoMatch means there is no match. NoMatch // FirstArgIsInvalidJSON means the first argument is invalid JSON. FirstArgIsInvalidJSON // SecondArgIsInvalidJSON means the second argument is invalid JSON. SecondArgIsInvalidJSON // BothArgsAreInvalidJSON means both arguments are invalid JSON. BothArgsAreInvalidJSON // ExpressionError means a {{...}} expression in the first argument could // not be rendered, for example because it references an unbound variable // with invalid template syntax. ExpressionError )
func Compare ¶
func Compare(a, b []byte, opts *DiffOptions) (Difference, string)
Compare compares two JSON documents using given options. Returns difference type and a string describing differences.
*FullMatch* means provided arguments are deeply equal.
*SupersetMatch* means first argument is a superset of a second argument. In this context being a superset means that for each object or array in the hierarchy which don't match exactly, it must be a superset of another one. For example:
{"a": 123, "b": 456, "c": [7, 8, 9]}
Is a superset of:
{"a": 123, "c": [7, 8]}
*SubsetMatch* means first argument is a subset of a second argument. In this context being a subset means that for each object or array in the hierarchy which don't match exactly, it must be a subset of another one. For example:
{"a": 123, "c": [7, 8]}
Is a subset of:
{"a": 123, "b": 456, "c": [7, 8, 9]}
*NoMatch* means there is no match.
The rest of the difference types mean that one of or both JSON documents are invalid JSON.
Returned string uses a format similar to pretty printed JSON to show the human-readable difference between provided JSON documents. It is important to understand that returned format is not a valid JSON and is not meant to be machine readable.
Strings on the a side may be placeholders: {{any}}, {{timestamp}} and {{uuid}} match a value by shape; any other {{name}} captures the b value and must match it wherever the name recurs; "{{x}}:{{y}}" and similar are rendered with text/template against the captured values.
Both documents are decoded with encoding/json (v1) so that numbers keep their literal text; json.Options passed to JSONEqual and friends affect only how the arguments are marshaled, not this comparison.
Example ¶
diff, rendered := gotestify.Compare(
[]byte(`{"a": 1, "b": "{{any}}", "c": [1, 2]}`),
[]byte(`{"a": 2, "b": null, "c": [1, 2, 3]}`),
gotestify.JSONDiffOptions(),
)
fmt.Println(diff)
fmt.Println(rendered)
Output: NoMatch { "a": {"changed":[1, 2]}, "b": {"changed":["{{any}}", null]}, "c": [ 1, 2, "prop-added":{3} ] }
func CompareStr ¶
func CompareStr(a, b string, opts *DiffOptions) (Difference, string)
CompareStr is Compare for string documents.
func CompareStreams ¶
func CompareStreams(a, b io.Reader, opts *DiffOptions) (Difference, string)
CompareStreams compares two JSON documents streamed by the specified readers. See the documentation for `Compare` for a description of the input options and return values.
func (Difference) String ¶
func (d Difference) String() string
String returns the string representation of the difference type.
type Number ¶
type Number string
Number is a JSON number kept as its literal text, so that 1 and 1.0 stay distinguishable and large integers keep their precision. It is what Compare passes to DiffOptions.CompareNumbers.