attest

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: May 1, 2023 License: MIT Imports: 7 Imported by: 0

README

attest

Build Report Card GoDoc

attest is a small package of type-safe assertion helpers. Under the hood, it uses cmp for equality testing and diffing. You may enjoy attest if you prefer:

  • Type safety: it's impossible to compare values with different types.
  • Brevity: assertions usually print diffs rather than full values.
  • Minimalism: just a few assertions, not a whole DSL.
  • Natural ordering: every assertion uses got == want order.
  • Interoperability: assertions work with any cmp.Option.

Installation

go get go.akshayshah.org/attest

Usage

package main

import (
  "testing"
  "time"

  "github.com/akshayjshah/attest"
)

func TestExample(t *testing.T) {
  attest.Equal(t, 1, 1)
  attest.NotEqual(t, 2, 1)
  attest.Approximately(
    t,
    time.Minute - 1, // got
    time.Minute,     // want
    time.Second,     // tolerance
  )
  attest.Zero(t, "")
  attest.Contains(t, []int{0, 1, 2}, 2)

  var err error
  attest.Ok(t, err)
  err = fmt.Errorf("read config: %w", io.EOF)
  attest.Error(t, err)
  attest.ErrorIs(t, err, io.EOF)

  // You can enrich the default failure message.
  attest.Equal(t, 1, 2, attest.Sprintf("integer %s", "addition"))

  // The next two assertions won't compile.
  attest.Equal(t, int64(1), int(1))
  attest.Approximately(t, 9, 10, 0.5)
}

Failed assertions usually print a diff. Here's an example using attest.Equal:

--- FAIL: TestEqual (0.00s)
    attest_test.go:58: got != want
        diff (+got, -want):
          attest.Point{
                X: 1,
        -       Y: 4.2,
        +       Y: 3.5,
          }

Status: Stable

This module is stable. It supports the two most recent major releases of Go.

Within those parameters, attest follows semantic versioning. No breaking changes will be made without incrementing the major version.

Offered under the MIT license.

Documentation

Overview

Package attest is a small, type-safe library of assertion helpers.

Under the hood, attest uses cmp to test equality and diff values. All of attest's assertions work with any cmp.Option.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Approximately

func Approximately[T Number](tb TB, got, want, delta T, opts ...Option) bool

Approximately asserts that got is within delta of want. For example,

pi := float64(22)/7
Approximately(t, pi, 3.14, 0.01)

asserts that our estimate of pi is between 3.13 and 3.15, exclusive.

Approximately works with any type whose underlying type is numeric, so it also works with time.Duration.

func Contains

func Contains[T any](tb TB, got []T, want T, opts ...Option) bool

Contains asserts that a slice contains a target element.

func Equal

func Equal[T any](tb TB, got, want T, opts ...Option) bool

Equal asserts that two values are equal.

func Error

func Error(tb TB, err error, opts ...Option) bool

Error asserts that the error is not nil.

func ErrorIs

func ErrorIs(tb TB, got, want error, opts ...Option) bool

ErrorIs asserts that got wraps want, using the same logic as the standard library's errors.Is.

func False

func False(tb TB, got bool, opts ...Option) bool

False asserts that a boolean is false.

func NotEqual

func NotEqual[T any](tb TB, got, want T, opts ...Option) bool

NotEqual asserts that two values are not equal.

func NotZero

func NotZero[T any](tb TB, got T, opts ...Option) bool

NotZero asserts that the value is non-zero.

func Ok

func Ok(tb TB, err error, opts ...Option) bool

Ok asserts that the error is nil.

func Panics

func Panics(tb TB, f func(), opts ...Option) (ret bool)

Panics asserts that the function panics.

func Subsequence

func Subsequence[T ~string | ~[]byte](tb TB, got, want T, opts ...Option) bool

Subsequence asserts that got contains the subsequence want.

Subsequence(t, "hello world", "hello")
Subsequence(t, []byte("deadbeef"), []byte("ee"))

func True

func True(tb TB, got bool, opts ...Option) bool

True asserts that a boolean is true.

func Zero

func Zero[T any](tb TB, got T, opts ...Option) bool

Zero asserts that the value is its type's zero value.

Types

type Number

type Number interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~float32 | ~float64
}

A Number is any type whose underlying type is one of Go's built-in integral or floating-point types.

type Option

type Option interface {
	// contains filtered or unexported methods
}

An Option configures assertions.

func Allow

func Allow(types ...any) Option

Allow configures the underlying cmp package to forcibly introspect unexported fields of the specified struct types. (By default, cmp errors when comparing structs with unexported fields.) Allow fails the test if called with anything other than a struct type (for example, a pointer or slice).

Allow is useful as a quick hack but is usually a bad idea: changes in the internals of some other package may break your tests. If you control the type in question, implement an Equal method instead. If you don't control the type, Comparer is usually safer.

Example
package main

import (
	"fmt"

	"go.akshayshah.org/attest"
)

// logTB implements the portions of the [testing.TB] interface that attest uses.
// It writes assertion failures to stdout.
type logTB struct{}

func (*logTB) Helper() {}

func (*logTB) Errorf(tmpl string, args ...any) {
	fmt.Printf("ERROR: "+tmpl, args...)
}

func (*logTB) Fatalf(tmpl string, args ...any) {
	fmt.Printf("FATAL: "+tmpl, args...)
}

type point struct {
	x, y float64
}

func main() {
	attest.Equal(
		&logTB{},
		point{1.0, 1.0},
		point{1.0, 1.0},
		// Without Allow, cmp errors because point has unexported fields. We could
		// also use [Comparer], or we could implement an Equal method on point.
		attest.Allow(point{}),
	)
}
Output:

func Cmp

func Cmp(opts ...cmp.Option) Option

Cmp configures the underlying equality assertion, if any. See the cmp package documentation for an explanation of the default logic.

In particular, note that the cmp package's default behavior is to error when comparing structs with unexported fields. If you control the type in question, implement an Equal method and cmp will use it by default. If you don't control the type, use Allow or Comparer. If none of those approaches fit your needs, cmp and its cmpopts subpackage offer many other ways to relax this safety check.

If you're comparing types generated from a Protocol Buffer schema, protocmp.Transform safely transforms them to a comparable, diffable type.

func Comparer

func Comparer[T any](equal func(T, T) bool) Option

Comparer configures the underlying cmp package to compare values of type T using the provided function. This is especially useful when comparing third-party types with unexported fields.

The equality function must be symmetric (the order of the two arguments doesn't matter), deterministic (it always returns the same result), and pure (it may not mutate its arguments).

Example
package main

import (
	"fmt"

	"go.akshayshah.org/attest"
)

// logTB implements the portions of the [testing.TB] interface that attest uses.
// It writes assertion failures to stdout.
type logTB struct{}

func (*logTB) Helper() {}

func (*logTB) Errorf(tmpl string, args ...any) {
	fmt.Printf("ERROR: "+tmpl, args...)
}

func (*logTB) Fatalf(tmpl string, args ...any) {
	fmt.Printf("FATAL: "+tmpl, args...)
}

type point struct {
	x, y float64
}

func main() {
	attest.Equal(
		&logTB{},
		point{1.0, 1.0},
		point{1.0, 1.0},
		// Without Comparer, cmp errors because point has unexported fields. We
		// could also use [Allow], or we could implement an Equal method on point.
		attest.Comparer(func(left, right point) bool {
			return left.x == right.x && left.y == right.y
		}),
	)
}
Output:

func Continue

func Continue() Option

Continue allows the test to continue executing when an assertion fails. By default, failed assertions stop the test immediately.

func Fatal

func Fatal() Option

Fatal stops the test immediately when an assertion fails. This is the default behavior, but Fatal may still be useful to reverse the effect of Continue.

func Options

func Options(opts ...Option) Option

Options composes multiple Options into one. This may be useful if you're writing a helper package that bundles several options together, or if most assertions in your tests use a common set of options.

Example
package main

import (
	"fmt"

	"go.akshayshah.org/attest"
)

// logTB implements the portions of the [testing.TB] interface that attest uses.
// It writes assertion failures to stdout.
type logTB struct{}

func (*logTB) Helper() {}

func (*logTB) Errorf(tmpl string, args ...any) {
	fmt.Printf("ERROR: "+tmpl, args...)
}

func (*logTB) Fatalf(tmpl string, args ...any) {
	fmt.Printf("FATAL: "+tmpl, args...)
}

type point struct {
	x, y float64
}

func main() {
	// If all our tests have some options in common, it's nice to extract them
	// into a named bundle.
	defaults := attest.Options(
		attest.Continue(),
		attest.Comparer(func(left, right point) bool {
			return left.x == right.x && left.y == right.y
		}),
	)
	// We can reuse our default options in each test, and we can add more options
	// without an ugly cascade of appends.
	attest.Zero(
		&logTB{},
		point{},
		defaults,       // our defaults
		attest.Fatal(), // override Continue from defaults
	)
}
Output:

func Sprint

func Sprint(args ...any) Option

Sprint adds an explanation to the default failure message. If your tests make many similar assertions, the additional explanation may clarify the test output.

Arguments are passed to fmt.Sprint for formatting.

Example
package main

import (
	"fmt"
	"time"

	"go.akshayshah.org/attest"
)

// logTB implements the portions of the [testing.TB] interface that attest uses.
// It writes assertion failures to stdout.
type logTB struct{}

func (*logTB) Helper() {}

func (*logTB) Errorf(tmpl string, args ...any) {
	fmt.Printf("ERROR: "+tmpl, args...)
}

func (*logTB) Fatalf(tmpl string, args ...any) {
	fmt.Printf("FATAL: "+tmpl, args...)
}

func main() {
	today := time.Now()
	tomorrow := today.Add(24 * time.Hour)
	attest.False(
		&logTB{},
		today.Before(tomorrow),
		attest.Sprint("alas, time", " marches on"),
	)
}
Output:

FATAL: got true, want false: alas, time marches on

func Sprintf

func Sprintf(template string, args ...any) Option

Sprintf adds an explanation to the default failure message. If your tests make many similar assertions, the additional explanation may clarify the test output.

Arguments are passed to fmt.Sprintf for formatting.

Example
package main

import (
	"fmt"
	"time"

	"go.akshayshah.org/attest"
)

// logTB implements the portions of the [testing.TB] interface that attest uses.
// It writes assertion failures to stdout.
type logTB struct{}

func (*logTB) Helper() {}

func (*logTB) Errorf(tmpl string, args ...any) {
	fmt.Printf("ERROR: "+tmpl, args...)
}

func (*logTB) Fatalf(tmpl string, args ...any) {
	fmt.Printf("FATAL: "+tmpl, args...)
}

func main() {
	today := time.Now()
	tomorrow := today.Add(24 * time.Hour)
	attest.False(
		&logTB{},
		today.Before(tomorrow),
		attest.Sprintf("%s, time marches on", "alas"),
	)
}
Output:

FATAL: got true, want false: alas, time marches on

type TB

type TB interface {
	Helper()
	Errorf(string, ...any)
	Fatalf(string, ...any)
}

TB is the subset of testing.TB that attest depends on. The standard library's *testing.T, B, and F types all implement it.

Jump to

Keyboard shortcuts

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