A Go testing toolkit with structured document comparison (JSON, YAML, HTML) and expressive assertions.
go get github.com/monkescience/testasticCompare API responses or rendered documents against expected files with template matchers.
testastic.AssertJSON(t, "testdata/user.expected.json", resp.Body)testastic.AssertYAML(t, "testdata/config.expected.yaml", configBytes)Expected files may contain YAML document streams:
kind: ConfigMapmetadata:
name: settings
---
kind: Deploymentmetadata:
name: apiStreams 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.
testastic.AssertHTML(t, "testdata/page.expected.html", renderedHTML)testastic.AssertFile(t, "testdata/output.expected.txt", actualString)Input types by assertion:
AssertJSONandAssertYAMLacceptstring,[]byte,io.Reader, or any struct (auto-marshaled).AssertHTMLacceptsstring,[]byte,io.Reader, orfmt.Stringer.AssertFileacceptsstring,[]byte, orio.Reader.
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:
| Matcher | Description |
|---|---|
{{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 |
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}}"
// 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())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"))When responses change, update expected files automatically:
go test -update
# or
TESTASTIC_UPDATE=true go testTestastic 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.
testastic.Equal(t, expected, actual)
testastic.NotEqual(t, unexpected, actual)
testastic.DeepEqual(t, expected, actual)testastic.Nil(t, value)
testastic.NotNil(t, value)
testastic.True(t, value)
testastic.False(t, value)testastic.NoError(t, err)
testastic.Error(t, err)
testastic.ErrorIs(t, err, target)
testastic.ErrorAs(t, err, &pathErr)
testastic.ErrorContains(t, err, "substring")testastic.Panics(t, func() { panic("boom") })
testastic.NotPanics(t, func() { safeFunc() })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)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.
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)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"),
)Start and test Go binaries as subprocesses with automatic coverage instrumentation.
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),
)| Option | Description |
|---|---|
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:
| Option | Description |
|---|---|
WithBuildArgs(args...) | Additional go build flags |
WithWorkDir(dir) | Working directory for go build |
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
}),
)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.outColored 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
}