Skip to content

Latest commit

History

238 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

testastic

A Go testing toolkit with structured document comparison (JSON, YAML, HTML) and expressive assertions.

Install

go get github.com/monkescience/testastic

Document Assertions

Compare API responses or rendered documents against expected files with template matchers.

JSON

testastic.AssertJSON(t, "testdata/user.expected.json", resp.Body)

YAML

testastic.AssertYAML(t, "testdata/config.expected.yaml", configBytes)

Expected files may contain YAML document streams:

kind: ConfigMapmetadata:
name: settings
---
kind: Deploymentmetadata:
name: api

Streams are compared document by document, including empty documents, and document order remains significant. Paths passed to IgnoreFields or IgnoreArrayOrderAt are relative to each document root, so $.metadata.name applies independently in every document.

HTML

testastic.AssertHTML(t, "testdata/page.expected.html", renderedHTML)

Plain Text Files

testastic.AssertFile(t, "testdata/output.expected.txt", actualString)

Input types by assertion:

  • AssertJSON and AssertYAML accept string, []byte, io.Reader, or any struct (auto-marshaled).
  • AssertHTML accepts string, []byte, io.Reader, or fmt.Stringer.
  • AssertFile accepts string, []byte, or io.Reader.

Matchers

Expected files support template matchers for dynamic values:

{
"id": "{{anyUUID}}",
"count": "{{anyInt}}",
"email": "{{regex `^[a-z]+@example\\.com$`}}",
"status": "{{oneOf \"pending\"\"active\"}}",
"timestamp": "{{ignore}}"
}

Built-in matchers:

MatcherDescription
{{anyString}}Matches any string
{{anyInt}}Matches any integer
{{anyFloat}}Matches any number
{{anyBool}}Matches any boolean
{{anyValue}}Matches any value including null
{{anyUUID}}Matches UUID strings (RFC 4122)
{{anyDateTime}}Matches ISO 8601 datetime strings
{{anyURL}}Matches URL strings
{{ignore}}Skips the field during comparison
{{regex \pattern`}}`Matches against a regular expression
{{oneOf "a" "b"}}Matches one of the specified values

Custom Matchers

Register custom matchers for domain-specific validation:

testastic.RegisterMatcher("orderID", func(argsstring) (testastic.Matcher, error) {
return&orderIDMatcher{}, nil
})

Then use in expected files: "id": "{{orderID}}"

Options

General Options

// Ignore specific fields by name or pathAssertJSON(t, expected, actual, IgnoreFields("id", "timestamp"))
AssertJSON(t, expected, actual, IgnoreFields("$.user.id"))
// Ignore array order globally or at specific pathsAssertJSON(t, expected, actual, IgnoreArrayOrder())
AssertJSON(t, expected, actual, IgnoreArrayOrderAt("$.items"))
// Add context to failure messagesAssertJSON(t, expected, actual, Message("user creation response"))
// Force update expected file programmaticallyAssertJSON(t, expected, actual, Update())

HTML-Specific Options

AssertHTML(t, expected, actual, IgnoreHTMLComments())
AssertHTML(t, expected, actual, PreserveWhitespace())
AssertHTML(t, expected, actual, IgnoreChildOrder())
AssertHTML(t, expected, actual, IgnoreChildOrderAt("html > body > ul"))
AssertHTML(t, expected, actual, IgnoreElements("script", "style"))
AssertHTML(t, expected, actual, IgnoreAttributes("class", "style"))
AssertHTML(t, expected, actual, IgnoreAttributeAt("html > body > div@id"))

Updating Expected Files

When responses change, update expected files automatically:

go test -update
# or
TESTASTIC_UPDATE=true go test

Testastic also follows standard terminal environment conventions when rendering diffs. NO_COLOR disables ANSI colors, FORCE_COLOR enables them, CI disables them by default, and TERM=dumb disables them. NO_COLOR takes precedence over the other settings.

General Assertions

Equality

testastic.Equal(t, expected, actual)
testastic.NotEqual(t, unexpected, actual)
testastic.DeepEqual(t, expected, actual)

Nil / Boolean

testastic.Nil(t, value)
testastic.NotNil(t, value)
testastic.True(t, value)
testastic.False(t, value)

Errors

testastic.NoError(t, err)
testastic.Error(t, err)
testastic.ErrorIs(t, err, target)
testastic.ErrorAs(t, err, &pathErr)
testastic.ErrorContains(t, err, "substring")

Panics

testastic.Panics(t, func() { panic("boom") })
testastic.NotPanics(t, func() { safeFunc() })

Comparison

testastic.Greater(t, a, b)
testastic.GreaterOrEqual(t, a, b)
testastic.Less(t, a, b)
testastic.LessOrEqual(t, a, b)
testastic.Between(t, value, min, max)

Strings

testastic.Contains(t, s, substring)
testastic.NotContains(t, s, substring)
testastic.HasPrefix(t, s, prefix)
testastic.NotHasPrefix(t, s, prefix)
testastic.HasSuffix(t, s, suffix)
testastic.NotHasSuffix(t, s, suffix)
testastic.Matches(t, s, `^\d+$`)
testastic.StringEmpty(t, s)
testastic.StringNotEmpty(t, s)

Contains, NotContains, HasPrefix, NotHasPrefix, HasSuffix, NotHasSuffix, and Matches accept string, []byte, or fmt.Stringer inputs.

Collections

testastic.Len(t, collection, expected)
testastic.Empty(t, collection)
testastic.NotEmpty(t, collection)
testastic.SliceContains(t, slice, element)
testastic.SliceNotContains(t, slice, element)
testastic.SliceEqual(t, expected, actual)
testastic.MapHasKey(t, m, key)
testastic.MapNotHasKey(t, m, key)
testastic.MapHasValue(t, m, value)
testastic.MapNotHasValue(t, m, value)
testastic.MapEqual(t, expected, actual)

Eventual Assertions

For asynchronous operations, retry until a condition is met or timeout is reached. The condition is checked immediately, then at regular intervals (default 100ms).

testastic.Eventually(t, func() bool {
returnserver.IsReady()
}, 5*time.Second)
testastic.EventuallyEqual(t, "ready", func() string {
returnservice.Status()
}, 3*time.Second)
testastic.EventuallyNoError(t, func() error {
_, err:=client.Ping()
returnerr
}, 5*time.Second)

All Eventually variants:

testastic.Eventually(t, conditionFn, timeout)
testastic.EventuallyTrue(t, conditionFn, timeout)
testastic.EventuallyFalse(t, conditionFn, timeout)
testastic.EventuallyEqual(t, expected, getValueFn, timeout)
testastic.EventuallyNil(t, getValueFn, timeout)
testastic.EventuallyNotNil(t, getValueFn, timeout)
testastic.EventuallyNoError(t, getErrFn, timeout)
testastic.EventuallyError(t, getErrFn, timeout)

Options:

testastic.Eventually(t, condition, 5*time.Second,
testastic.WithInterval(50*time.Millisecond),
testastic.WithMessage("waiting for server"),
)

Process Testing

Start and test Go binaries as subprocesses with automatic coverage instrumentation.

Starting a Process

Build the binary once, then reuse it across tests:

varapiBinary*testastic.BinaryfuncTestMain(m*testing.M) {
apiBinary=testastic.BuildBinaryMain(m, "./cmd/api",
testastic.WithBuildArgs("-tags", "integration"),
)
code:=testastic.CollectSubprocessCoverage(m, "coverage/process.out")
apiBinary.Cleanup()
os.Exit(code)
}
funcTestAPI(t*testing.T) {
proc:=apiBinary.Start(t.Context(), t,
testastic.HTTPCheck(8080, "/health"),
testastic.WithPort(8080),
testastic.WithEnv("DATABASE_URL=postgres://localhost/test"),
)
resp, err:=http.Get(proc.URL() +"/api/users")
testastic.NoError(t, err)
deferresp.Body.Close()
testastic.AssertJSON(t, "testdata/users.expected.json", resp.Body)
}

For pre-built binaries, open them and start them explicitly:

proc:=testastic.NewBinary(binaryPath).Start(t.Context(), t,
testastic.HTTPCheck(8080, "/health"),
testastic.WithPort(8080),
)

Process Options

OptionDescription
WithPort(port)TCP port; enables proc.URL()
WithEnv(vars...)Environment variables ("KEY=VALUE")
WithArgs(args...)Command-line arguments
WithReadyTimeout(d)Readiness timeout (default: 10s)
WithReadyInterval(d)Readiness poll interval (default: 100ms)
WithShutdownTimeout(d)Graceful shutdown timeout (default: 5s)
WithCoverDir(dir)Override coverage data directory
WithWorkDir(dir)Working directory for build and process start

Build options:

OptionDescription
WithBuildArgs(args...)Additional go build flags
WithWorkDir(dir)Working directory for go build

Custom Ready Checks

Use ReadyCheckFunc for custom readiness logic:

workerBinary:=testastic.BuildBinary(t, "./cmd/worker")
proc:=workerBinary.Start(t.Context(), t,
testastic.ReadyCheckFunc(func(ctx context.Context) bool {
conn, err:=net.DialTimeout("tcp", "localhost:6379", time.Second)
iferr!=nil {
returnfalse
}
conn.Close()
returntrue
}),
)

Collecting Coverage

By default, subprocess coverage data is written to a temp directory and cleaned up. To collect it into a standard Go coverage profile, add a TestMain:

funcTestMain(m*testing.M) {
exitCode:=testastic.CollectSubprocessCoverage(m, "coverage/process.out")
cleanup()
os.Exit(exitCode)
}

Run any package-level cleanup after CollectSubprocessCoverage returns and before os.Exit(code). Avoid relying on defer for that cleanup, because os.Exit exits immediately without running deferred functions.

This produces a text profile compatible with go tool cover, codecov, and coveralls:

go tool cover -html=coverage/process.out

Output

Colored diff output (red for expected, green for actual):

testastic: assertion failed
Equal
expected: "Alice"
actual: "Bob"

JSON/YAML mismatches show git-style inline diff:

testastic: assertion failed
AssertJSON (testdata/user.expected.json)
{
- "name": "Alice",+ "name": "Bob",
"age": 30
}

About

Go testing toolkit with structured document comparison (JSON, YAML, HTML) and expressive assertions

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages