testastic

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Feb 11, 2026 License: MIT Imports: 20 Imported by: 0

README

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)
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:

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
Custom Matchers

Register custom matchers for domain-specific validation:

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

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

Options

General Options
// Ignore specific fields by name or path
AssertJSON(t, expected, actual, IgnoreFields("id", "timestamp"))
AssertJSON(t, expected, actual, IgnoreFields("$.user.id"))

// Ignore array order globally or at specific paths
AssertJSON(t, expected, actual, IgnoreArrayOrder())
AssertJSON(t, expected, actual, IgnoreArrayOrderAt("$.items"))

// Add context to failure messages
AssertJSON(t, expected, actual, Message("user creation response"))

// Force update expected file programmatically
AssertJSON(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

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.HasSuffix(t, s, suffix)
testastic.Matches(t, s, `^\d+$`)
testastic.StringEmpty(t, s)
testastic.StringNotEmpty(t, s)
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.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 {
    return server.IsReady()
}, 5*time.Second)

testastic.EventuallyEqual(t, "ready", func() string {
    return service.Status()
}, 3*time.Second)

testastic.EventuallyNoError(t, func() error {
    _, err := client.Ping()
    return err
}, 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"),
)

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
  }

Documentation

Overview

Package testastic provides expressive testing utilities for Go.

Testastic offers structured comparison for JSON, YAML, HTML, and plain text documents with support for template-based matchers, alongside general-purpose assertions for values, errors, strings, and collections.

JSON, YAML, HTML, and File Assertions

Compare API responses or rendered documents against expected files with flexible matching:

testastic.AssertJSON(t, "testdata/user.expected.json", resp.Body)
testastic.AssertYAML(t, "testdata/config.expected.yaml", configBytes)
testastic.AssertHTML(t, "testdata/page.expected.html", renderedHTML)
testastic.AssertFile(t, "testdata/output.expected.txt", actualString)

Input types by assertion:

  • AssertJSON and AssertYAML: string, []byte, io.Reader, or any struct (auto-marshaled)
  • AssertHTML: string, []byte, io.Reader, or fmt.Stringer
  • AssertFile: string, []byte, or io.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}}"
}

Available built-in matchers:

  • {{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

Comparison Options

Configure comparison behavior with options:

testastic.AssertJSON(t, expected, actual,
    testastic.IgnoreArrayOrder(),           // ignore order globally
    testastic.IgnoreArrayOrderAt("$.items"), // ignore order at path
    testastic.IgnoreFields("id", "timestamp"), // exclude fields
    testastic.Message("user creation response"), // custom failure message
)

HTML-Specific Options

Additional options are available for HTML comparison:

testastic.AssertHTML(t, expected, actual,
    testastic.IgnoreHTMLComments(),                     // exclude comments
    testastic.PreserveWhitespace(),                     // disable whitespace normalization
    testastic.IgnoreChildOrder(),                       // order-insensitive globally
    testastic.IgnoreChildOrderAt("html > body > ul"),   // order-insensitive at path
    testastic.IgnoreElements("script", "style"),        // exclude elements by tag
    testastic.IgnoreAttributes("class", "style"),       // exclude attributes globally
    testastic.IgnoreAttributeAt("html > body > div@id"), // exclude attribute at path
)

Updating Expected Files

When API responses change, update expected files automatically:

go test -update
# or
TESTASTIC_UPDATE=true go test

Basic Assertions

General-purpose assertions for common test scenarios:

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, condition)
testastic.False(t, condition)

Error Assertions

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

Panic Assertions

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

Comparison Assertions

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)

String Assertions

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

Collection Assertions

testastic.Len(t, slice, 3)
testastic.Empty(t, slice)
testastic.NotEmpty(t, slice)
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.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 {
    return server.IsReady()
}, 5*time.Second)

testastic.EventuallyTrue(t, func() bool {
    return isReady
}, 3*time.Second)

testastic.EventuallyFalse(t, func() bool {
    return server.IsProcessing()
}, 5*time.Second)

testastic.EventuallyEqual(t, "ready", func() string {
    return service.Status()
}, 3*time.Second, testastic.WithInterval(50*time.Millisecond))

testastic.EventuallyNil(t, func() any {
    return cache.Get("key")
}, 2*time.Second)

testastic.EventuallyNotNil(t, func() any {
    return cache.Get("key")
}, 2*time.Second)

testastic.EventuallyNoError(t, func() error {
    _, err := client.Ping()
    return err
}, 5*time.Second)

testastic.EventuallyError(t, func() error {
    return service.HealthCheck()
}, 3*time.Second)

Configure polling with WithInterval and WithMessage:

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

Custom Matchers

Register custom matchers for domain-specific validation:

testastic.RegisterMatcher("customID", func(args string) (testastic.Matcher, error) {
    return myCustomMatcher{}, nil
})

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

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertFile

func AssertFile[T any](tb testing.TB, expectedFile string, actual T, opts ...Option)

AssertFile compares actual content against an expected file with template matcher support. Supports {{anyString}}, {{regex `pattern`}}, and other matchers inline within text.

Supported actual types: string, []byte, or io.Reader.

func AssertHTML

func AssertHTML[T any](tb testing.TB, expectedFile string, actual T, opts ...Option)

AssertHTML compares actual HTML against an expected HTML file. T can be: []byte, string, io.Reader, or any type implementing fmt.Stringer.

Example:

testastic.AssertHTML(t, "testdata/user.expected.html", resp.Body)
testastic.AssertHTML(t, "testdata/user.expected.html", htmlBytes)
testastic.AssertHTML(t, "testdata/user.expected.html", htmlString)

func AssertJSON

func AssertJSON[T any](tb testing.TB, expectedFile string, actual T, opts ...Option)

AssertJSON compares actual JSON against an expected JSON file. T can be: []byte, string, io.Reader, or any struct (auto-marshaled).

Example:

testastic.AssertJSON(t, "testdata/user.expected.json", resp.Body)
testastic.AssertJSON(t, "testdata/user.expected.json", myUser)
testastic.AssertJSON(t, "testdata/user.expected.json", jsonBytes)
Example
package main

import (
	"fmt"
)

func main() {
	// In a test function:
	//
	//   func TestUserAPI(t *testing.T) {
	//       resp, _ := http.Get("/api/user/123")
	//       defer resp.Body.Close()
	//
	//       // Compare response body against expected file
	//       testastic.AssertJSON(t, "testdata/user.expected.json", resp.Body)
	//   }
	//
	// The expected file can contain matchers for dynamic values:
	//
	//   {
	//     "id": "{{anyUUID}}",
	//     "name": "Alice",
	//     "email": "{{regex `^[a-z]+@example\\.com$`}}",
	//     "createdAt": "{{anyDateTime}}",
	//     "role": "{{oneOf \"admin\" \"user\"}}"
	//   }
	//
	// AssertJSON accepts []byte, string, io.Reader, or any struct:
	//
	//   testastic.AssertJSON(t, "testdata/user.expected.json", userStruct)
	//   testastic.AssertJSON(t, "testdata/user.expected.json", jsonBytes)
	//   testastic.AssertJSON(t, "testdata/user.expected.json", jsonString)
	fmt.Println("See godoc for usage examples")
}
Output:

See godoc for usage examples
Example (WithOptions)
package main

import (
	"fmt"
)

func main() {
	// Use options to customize comparison behavior:
	//
	//   func TestListUsers(t *testing.T) {
	//       resp := getUsers()
	//
	//       // Ignore array order when comparing lists
	//       testastic.AssertJSON(t, "testdata/users.expected.json", resp,
	//           testastic.IgnoreArrayOrder(),
	//       )
	//
	//       // Ignore order only at specific paths
	//       testastic.AssertJSON(t, "testdata/response.expected.json", resp,
	//           testastic.IgnoreArrayOrderAt("$.data.items"),
	//           testastic.IgnoreArrayOrderAt("$.data.tags"),
	//       )
	//
	//       // Exclude volatile fields from comparison
	//       testastic.AssertJSON(t, "testdata/user.expected.json", resp,
	//           testastic.IgnoreFields("id", "createdAt", "updatedAt"),
	//       )
	//
	//       // Add context to failure messages
	//       testastic.AssertJSON(t, "testdata/user.expected.json", resp,
	//           testastic.Message("user creation response"),
	//       )
	//
	//       // Combine multiple options
	//       testastic.AssertJSON(t, "testdata/complex.expected.json", resp,
	//           testastic.IgnoreArrayOrder(),
	//           testastic.IgnoreFields("timestamp"),
	//           testastic.Message("complex response validation"),
	//       )
	//   }
	//
	// Update expected files when API changes:
	//
	//   go test -update
	//   # or use the Update option programmatically:
	//   testastic.AssertJSON(t, expected, actual, testastic.Update())
	fmt.Println("See godoc for usage examples")
}
Output:

See godoc for usage examples

func AssertYAML

func AssertYAML[T any](tb testing.TB, expectedFile string, actual T, opts ...Option)

AssertYAML compares actual YAML against an expected YAML file. T can be: []byte, string, io.Reader, or any struct (auto-marshaled).

Example:

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

func Between

func Between[T cmp.Ordered](tb testing.TB, value, minVal, maxVal T)

Between asserts that minVal <= value <= maxVal.

func Contains

func Contains(tb testing.TB, s, substring string)

Contains asserts that s contains the given substring. Reports a non-fatal error on failure, displaying the string and missing substring.

func DeepEqual

func DeepEqual[T any](tb testing.TB, expected, actual T)

DeepEqual asserts that expected and actual are deeply equal using reflect.DeepEqual.

func Empty

func Empty(tb testing.TB, collection any)

Empty asserts that the collection is empty. Works with slices, maps, strings, arrays, and channels.

func Equal

func Equal[T comparable](tb testing.TB, expected, actual T)

Equal asserts that expected and actual are equal using the == operator. Reports a non-fatal error on failure, allowing the test to continue.

Example
package main

import (
	"fmt"
)

func main() {
	// In a test function:
	//
	//   func TestCalculation(t *testing.T) {
	//       result := Calculate(10, 20)
	//       testastic.Equal(t, 30, result)
	//
	//       name := GetUserName(123)
	//       testastic.Equal(t, "Alice", name)
	//   }
	//
	// For complex types, use DeepEqual:
	//
	//   func TestStructEquality(t *testing.T) {
	//       expected := User{Name: "Alice", Age: 30}
	//       actual := GetUser(123)
	//       testastic.DeepEqual(t, expected, actual)
	//   }
	//
	// Related assertions:
	//
	//   testastic.NotEqual(t, unexpected, actual)  // values must differ
	//   testastic.Nil(t, value)                    // value must be nil
	//   testastic.NotNil(t, value)                 // value must not be nil
	//   testastic.True(t, condition)               // condition must be true
	//   testastic.False(t, condition)              // condition must be false
	fmt.Println("See godoc for usage examples")
}
Output:

See godoc for usage examples

func Error

func Error(tb testing.TB, err error)

Error asserts that err is not nil. Reports a non-fatal error on failure, allowing the test to continue.

func ErrorAs added in v0.1.0

func ErrorAs(tb testing.TB, err error, target any)

ErrorAs asserts that err matches the target type using errors.As. The target must be a non-nil pointer to either an error interface or a concrete type that implements error.

var pathErr *os.PathError
testastic.ErrorAs(t, err, &pathErr)

func ErrorContains

func ErrorContains(tb testing.TB, err error, substring string)

ErrorContains asserts that err is non-nil and its message contains the given substring.

func ErrorIs

func ErrorIs(tb testing.TB, err, target error)

ErrorIs asserts that err matches target using errors.Is.

func Eventually

func Eventually(tb testing.TB, condition func() bool, timeout time.Duration, opts ...EventuallyOption)

Eventually retries a condition function until it returns true or timeout is reached. The condition is checked immediately, then at regular intervals (default 100ms).

Example:

testastic.Eventually(t, func() bool {
    return server.IsReady()
}, 5*time.Second)

testastic.Eventually(t, func() bool {
    return len(queue) > 0
}, 2*time.Second, testastic.WithInterval(50*time.Millisecond))
Example
package main

import (
	"fmt"
)

func main() {
	// In a test function, wait for an async condition:
	//
	//   func TestServerStartup(t *testing.T) {
	//       go startServer()
	//
	//       // Wait up to 5 seconds for server to be ready
	//       testastic.Eventually(t, func() bool {
	//           resp, err := http.Get("http://localhost:8080/health")
	//           return err == nil && resp.StatusCode == 200
	//       }, 5*time.Second)
	//   }
	//
	// Customize polling interval:
	//
	//   testastic.Eventually(t, condition, 5*time.Second,
	//       testastic.WithInterval(50*time.Millisecond),  // check every 50ms
	//       testastic.WithMessage("waiting for server"),  // add context to failures
	//   )
	//
	// Type-specific variants provide better error messages:
	//
	//   // Wait for specific value
	//   testastic.EventuallyEqual(t, "ready", func() string {
	//       return service.Status()
	//   }, 3*time.Second)
	//
	//   // Wait for no error
	//   testastic.EventuallyNoError(t, func() error {
	//       _, err := client.Ping()
	//       return err
	//   }, 5*time.Second)
	//
	//   // Wait for condition to become false
	//   testastic.EventuallyFalse(t, func() bool {
	//       return server.IsProcessing()
	//   }, 10*time.Second)
	fmt.Println("See godoc for usage examples")
}
Output:

See godoc for usage examples

func EventuallyEqual

func EventuallyEqual[T comparable](
	tb testing.TB, expected T, getValue func() T, timeout time.Duration, opts ...EventuallyOption,
)

EventuallyEqual retries until expected equals the result of getValue.

Example:

testastic.EventuallyEqual(t, "ready", func() string {
    return service.Status()
}, 3*time.Second)

func EventuallyError

func EventuallyError(tb testing.TB, getErr func() error, timeout time.Duration, opts ...EventuallyOption)

EventuallyError retries until getErr returns a non-nil error.

Example:

testastic.EventuallyError(t, func() error {
    return service.HealthCheck()
}, 3*time.Second)

func EventuallyFalse

func EventuallyFalse(tb testing.TB, condition func() bool, timeout time.Duration, opts ...EventuallyOption)

EventuallyFalse retries until condition returns false.

Example:

testastic.EventuallyFalse(t, func() bool {
    return server.IsProcessing()
}, 5*time.Second)

func EventuallyNil

func EventuallyNil(tb testing.TB, getValue func() any, timeout time.Duration, opts ...EventuallyOption)

EventuallyNil retries until getValue returns nil.

Example:

testastic.EventuallyNil(t, func() any {
    return cache.Get("key")
}, 2*time.Second)

func EventuallyNoError

func EventuallyNoError(tb testing.TB, getErr func() error, timeout time.Duration, opts ...EventuallyOption)

EventuallyNoError retries until getErr returns nil.

Example:

testastic.EventuallyNoError(t, func() error {
    _, err := client.Ping()
    return err
}, 5*time.Second)

func EventuallyNotNil

func EventuallyNotNil(tb testing.TB, getValue func() any, timeout time.Duration, opts ...EventuallyOption)

EventuallyNotNil retries until getValue returns a non-nil value.

Example:

testastic.EventuallyNotNil(t, func() any {
    return cache.Get("key")
}, 2*time.Second)

func EventuallyTrue

func EventuallyTrue(tb testing.TB, condition func() bool, timeout time.Duration, opts ...EventuallyOption)

EventuallyTrue is an alias for Eventually for readability.

Example:

testastic.EventuallyTrue(t, func() bool {
    return isReady
}, 3*time.Second)

func False

func False(tb testing.TB, value bool)

False asserts that value is false. Reports a non-fatal error on failure, allowing the test to continue.

func Greater

func Greater[T cmp.Ordered](tb testing.TB, a, b T)

Greater asserts that a > b using cmp.Ordered comparison.

func GreaterOrEqual

func GreaterOrEqual[T cmp.Ordered](tb testing.TB, a, b T)

GreaterOrEqual asserts that a >= b using cmp.Ordered comparison.

func HasPrefix

func HasPrefix(tb testing.TB, s, prefix string)

HasPrefix asserts that s starts with the given prefix.

func HasSuffix

func HasSuffix(tb testing.TB, s, suffix string)

HasSuffix asserts that s ends with the given suffix.

func Len

func Len(tb testing.TB, collection any, expected int)

Len asserts that the collection has the expected length. Works with slices, maps, strings, arrays, and channels.

func Less

func Less[T cmp.Ordered](tb testing.TB, a, b T)

Less asserts that a < b using cmp.Ordered comparison.

func LessOrEqual

func LessOrEqual[T cmp.Ordered](tb testing.TB, a, b T)

LessOrEqual asserts that a <= b using cmp.Ordered comparison.

func MapEqual

func MapEqual[K comparable, V comparable](tb testing.TB, expected, actual map[K]V)

MapEqual asserts that two maps have the same keys and values using == comparison. Reports the first differing key found.

func MapHasKey

func MapHasKey[K comparable, V any](tb testing.TB, m map[K]V, key K)

MapHasKey asserts that the map contains the given key using map index lookup.

func MapHasValue added in v0.1.0

func MapHasValue[K comparable, V comparable](tb testing.TB, m map[K]V, value V)

MapHasValue asserts that the map contains the given value using == comparison. Performs a linear scan of all map values.

func MapNotHasKey

func MapNotHasKey[K comparable, V any](tb testing.TB, m map[K]V, key K)

MapNotHasKey asserts that the map does not contain the given key using map index lookup.

func Matches

func Matches(tb testing.TB, s, pattern string)

Matches asserts that s matches the given regular expression pattern.

func Nil

func Nil(tb testing.TB, value any)

Nil asserts that value is nil. Handles interface nil correctly by checking the underlying pointer for pointer, map, slice, channel, and func types.

func NoError

func NoError(tb testing.TB, err error)

NoError asserts that err is nil. Reports a non-fatal error on failure, displaying the error message.

func NotContains

func NotContains(tb testing.TB, s, substring string)

NotContains asserts that s does not contain the given substring.

func NotEmpty

func NotEmpty(tb testing.TB, collection any)

NotEmpty asserts that the collection is not empty. Works with slices, maps, strings, arrays, and channels.

func NotEqual

func NotEqual[T comparable](tb testing.TB, unexpected, actual T)

NotEqual asserts that expected and actual are not equal using the == operator. Reports a non-fatal error on failure, allowing the test to continue.

func NotNil

func NotNil(tb testing.TB, value any)

NotNil asserts that value is not nil. Handles interface nil correctly by checking the underlying pointer for pointer, map, slice, channel, and func types.

func NotPanics added in v0.1.0

func NotPanics(tb testing.TB, fn func())

NotPanics asserts that the function does not panic when called.

testastic.NotPanics(t, func() {
    doSomethingSafe()
})

func Panics added in v0.1.0

func Panics(tb testing.TB, fn func())

Panics asserts that the function panics when called.

testastic.Panics(t, func() {
    panic("something went wrong")
})

func RegisterMatcher

func RegisterMatcher(name string, factory MatcherFactory)

RegisterMatcher registers a custom matcher factory with the given name. Once registered, the matcher can be used in expected files as {{name}} or {{name args}}.

Registration should typically happen in TestMain or an init function to ensure matchers are available before tests run.

Example without arguments:

testastic.RegisterMatcher("orderID", func(args string) (testastic.Matcher, error) {
    return &orderIDMatcher{}, nil
})
// Use in expected file: "id": "{{orderID}}"

Example with arguments:

testastic.RegisterMatcher("minLength", func(args string) (testastic.Matcher, error) {
    n, err := strconv.Atoi(strings.TrimSpace(args))
    if err != nil {
        return nil, fmt.Errorf("minLength requires integer argument: %w", err)
    }
    return &minLengthMatcher{min: n}, nil
})
// Use in expected file: "name": "{{minLength 3}}"

Custom matchers must implement the Matcher interface.

Example
package main

import (
	"fmt"

	"github.com/monkescience/testastic"
)

func main() {
	// Register a custom matcher for domain-specific validation.
	// This is typically done in TestMain or an init function.

	// Example: matcher for order IDs with format "ORD-XXXXXX"
	testastic.RegisterMatcher("orderID", func(args string) (testastic.Matcher, error) {
		return &orderIDMatcher{}, nil
	})

	// Example: matcher with arguments for currency amounts
	testastic.RegisterMatcher("currency", func(args string) (testastic.Matcher, error) {
		// args contains everything after the matcher name
		// e.g., for {{currency USD}}, args would be "USD"
		return &currencyMatcher{currency: args}, nil
	})

	// Now use in expected JSON files:
	//
	//   {
	//     "orderId": "{{orderID}}",
	//     "total": "{{currency USD}}"
	//   }

	fmt.Println("Custom matchers registered")
}

// orderIDMatcher matches order IDs with format "ORD-XXXXXX".
type orderIDMatcher struct{}

func (m *orderIDMatcher) Match(actual any) bool {
	s, ok := actual.(string)
	if !ok || len(s) != 10 {
		return false
	}

	return s[:4] == "ORD-"
}

func (m *orderIDMatcher) String() string {
	return "{{orderID}}"
}

// currencyMatcher matches currency amounts for a specific currency.
type currencyMatcher struct {
	currency string
}

func (m *currencyMatcher) Match(actual any) bool {

	switch v := actual.(type) {
	case float64:
		return v >= 0
	case int:
		return v >= 0
	}

	return false
}

func (m *currencyMatcher) String() string {
	return fmt.Sprintf("{{currency %s}}", m.currency)
}
Output:

Custom matchers registered

func SliceContains

func SliceContains[T comparable](tb testing.TB, slice []T, element T)

SliceContains asserts that slice contains element using == comparison. Reports a non-fatal error on failure, allowing the test to continue.

func SliceEqual

func SliceEqual[T comparable](tb testing.TB, expected, actual []T)

SliceEqual asserts that two slices are equal (same length and elements in same order).

func SliceNotContains

func SliceNotContains[T comparable](tb testing.TB, slice []T, element T)

SliceNotContains asserts that slice does not contain element using == comparison.

func StringEmpty

func StringEmpty(tb testing.TB, s string)

StringEmpty asserts that s is an empty string ("").

func StringNotEmpty

func StringNotEmpty(tb testing.TB, s string)

StringNotEmpty asserts that s is not an empty string ("").

func True

func True(tb testing.TB, value bool)

True asserts that value is true. Reports a non-fatal error on failure, allowing the test to continue.

Types

type EventuallyOption

type EventuallyOption func(*eventuallyConfig)

EventuallyOption configures Eventually behavior using functional options (see WithInterval and WithMessage).

func WithInterval

func WithInterval(d time.Duration) EventuallyOption

WithInterval sets the polling interval between condition checks. Default is 100ms.

func WithMessage

func WithMessage(msg string) EventuallyOption

WithMessage adds context to the timeout failure message, helping identify which Eventually assertion failed when a test has multiple.

type Matcher

type Matcher interface {
	// Match returns true if the actual value matches the expected pattern.
	Match(actual any) bool
	// String returns a description of what this matcher expects.
	String() string
}

Matcher defines the interface for custom value matching.

func AnyBool

func AnyBool() Matcher

AnyBool returns a Matcher that accepts any boolean value.

func AnyDateTime

func AnyDateTime() Matcher

AnyDateTime returns a matcher that matches ISO 8601 datetime strings.

func AnyFloat

func AnyFloat() Matcher

AnyFloat returns a Matcher that accepts any numeric value (int or float types).

func AnyInt

func AnyInt() Matcher

AnyInt returns a Matcher that accepts any integer value, including float64 values with no fractional part (as produced by JSON unmarshaling).

func AnyString

func AnyString() Matcher

AnyString returns a Matcher that accepts any string value.

func AnyURL

func AnyURL() Matcher

AnyURL returns a Matcher that accepts strings matching common HTTP/HTTPS URL patterns.

func AnyUUID

func AnyUUID() Matcher

AnyUUID returns a matcher that matches UUID strings (RFC 4122).

func AnyValue

func AnyValue() Matcher

AnyValue returns a matcher that matches any value including null.

func Ignore

func Ignore() Matcher

Ignore returns a matcher that causes the field to be skipped.

func OneOf

func OneOf(values ...any) Matcher

OneOf returns a Matcher that accepts values equal to one of the given values.

func Regex

func Regex(pattern string) (Matcher, error)

Regex returns a Matcher that accepts strings matching the given regular expression. Returns an error if the pattern fails to compile.

type MatcherFactory

type MatcherFactory func(args string) (Matcher, error)

MatcherFactory creates a Matcher from arguments extracted from a template expression. The args parameter contains everything after the matcher name in the template.

For {{myMatcher}}, args is an empty string. For {{myMatcher foo bar}}, args is "foo bar".

Return an error if the arguments are invalid. The error will be wrapped and reported during template parsing.

type Option added in v0.1.0

type Option func(*config)

Option configures the behavior of assertion functions. Use the provided option constructors (IgnoreFields, IgnoreArrayOrder, etc.) to create options.

func IgnoreArrayOrder

func IgnoreArrayOrder() Option

IgnoreArrayOrder makes array comparison order-insensitive globally. When enabled, arrays are compared as sets: [1, 2, 3] equals [3, 1, 2].

Use IgnoreArrayOrderAt to apply order-insensitive comparison only at specific paths.

func IgnoreArrayOrderAt

func IgnoreArrayOrderAt(path string) Option

IgnoreArrayOrderAt makes array comparison order-insensitive at the specified path. Unlike IgnoreArrayOrder, this only affects the array at the given path.

Path format uses JSONPath-like syntax:

IgnoreArrayOrderAt("$.items")          // root-level items array
IgnoreArrayOrderAt("$.users[0].roles") // roles array in first user

The option also applies to nested arrays within the specified path.

func IgnoreAttributeAt

func IgnoreAttributeAt(pathAttr string) Option

IgnoreAttributeAt excludes a specific attribute at a given HTML path. The pathAttr format is "path@attribute", e.g., "html > body > div@class".

func IgnoreAttributes

func IgnoreAttributes(attrs ...string) Option

IgnoreAttributes excludes the specified attribute names from HTML comparison globally.

func IgnoreChildOrder

func IgnoreChildOrder() Option

IgnoreChildOrder makes child element comparison order-insensitive globally in HTML. Child elements are compared as sets rather than sequences.

func IgnoreChildOrderAt

func IgnoreChildOrderAt(path string) Option

IgnoreChildOrderAt makes child element comparison order-insensitive at the specified HTML path.

func IgnoreElements

func IgnoreElements(tags ...string) Option

IgnoreElements excludes elements matching the specified tag names from HTML comparison. Tag matching is case-insensitive.

func IgnoreFields

func IgnoreFields(fields ...string) Option

IgnoreFields excludes the specified fields from comparison. Fields can be specified by name (matches at any depth) or by full path.

By name (matches all occurrences):

IgnoreFields("id", "timestamp")  // ignores all "id" and "timestamp" fields

By path (matches specific locations):

IgnoreFields("$.user.id", "$.metadata.createdAt")

Path format uses JSONPath-like syntax:

  • $ represents the root
  • .field accesses object properties
  • [0] accesses array elements

func IgnoreHTMLComments

func IgnoreHTMLComments() Option

IgnoreHTMLComments excludes HTML comment nodes (<!-- ... -->) from comparison.

func Message

func Message(msg string) Option

Message adds a custom message to the assertion failure output. This helps identify which assertion failed when a test has multiple assertions.

testastic.AssertJSON(t, expected, actual,
    testastic.Message("user creation response"),
)

func PreserveWhitespace

func PreserveWhitespace() Option

PreserveWhitespace disables whitespace normalization in HTML comparison.

func Update

func Update() Option

Update forces updating the expected file with the actual value. This is equivalent to running tests with the -update flag or setting TESTASTIC_UPDATE=true.

Use this option when you need to programmatically update expected files:

if shouldUpdateGoldenFiles {
    testastic.AssertJSON(t, expected, actual, testastic.Update())
}

Jump to

Keyboard shortcuts

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