test

package module
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Jan 13, 2025 License: MIT Imports: 11 Imported by: 0

README

test

License Go Reference Go Report Card GitHub CI codecov

A lightweight test helper package 🧪

Project Description

test is my take on a handy, lightweight Go test helper package. Inspired by matryer/is, earthboundkid/be and others.

It provides a lightweight, but useful, extension to the std lib testing package with a friendlier and hopefully intuitive API. You definitely don't need it, but might find it useful anyway 🙂

Installation

go get github.com/FollowTheProcess/test@latest

Usage

test is as easy as...

func TestSomething(t *testing.T) {
    test.Equal(t, "hello", "hello") // Obviously fine
    test.Equal(t, "hello", "there") // Fails

    test.NotEqual(t, 42, 27) // Passes, these are not equal
    test.NotEqual(t, 42, 42) // Fails

    test.NearlyEqual(t, 3.0000000001, 3.0) // Look, floats handled easily!

    err := doSomething()
    test.Ok(t, err) // Fails if err != nil
    test.Err(t, err) // Fails if err == nil

    test.True(t, true) // Passes
    test.False(t, true) // Fails
}
Add Additional Context

test provides a number of options to decorate your test log with useful context:

func TestDetail(t *testing.T) {
    test.Equal(t, "apples", "oranges", test.Title("Fruit scramble!"), test.Context("Apples are not oranges!"))
}

Will get you an error log in the test that looks like this...

--- FAIL: TestDemo (0.00s)
    test_test.go:501: 
        Fruit scramble!
        ---------------
        
        Got:    apples
        Wanted: oranges
        
        (Apples are not oranges!)
        
FAIL
Non Comparable Types

test uses generics under the hood for most of the comparison, which is great, but what if your types don't satisfy comparable. We also provide test.EqualFunc and test.NotEqualFunc for those exact situations!

These allow you to pass in a custom comparator function for your type, if your comparator function returns true, the types are considered equal.

func TestNonComparableTypes(t *testing.T) {
    // Slices do not satisfy comparable
    a := []string{"hello", "there"}
    b := []string{"hello", "there"}
    c := []string{"general", "kenobi"}

    // Custom function, returns true if things should be considered equal
    sliceEqual := func(a, b, []string) { return true } // Cheating

    test.EqualFunc(t, a, b, sliceEqual) // Passes

    // Can also use any function here
    test.EqualFunc(t, a, b, slices.Equal) // Also passes :)

    test.EqualFunc(t, a, c, slices.Equal) // Fails
}

You can also use this same pattern for custom user defined types, structs etc.

Table Driven Tests

Table driven tests are great! But when you test errors too it can get a bit awkward, you have to do the if (err != nil) != tt.wantErr thing and I personally always have to do the boolean logic in my head to make sure I got that right. Enter test.WantErr:

func TestTableThings(t *testing.T) {
    tests := []struct {
        name    string
        want    int
        wantErr bool
    }{
        {
            name:    "no error",
            want:    4,
            wantErr: false,
        },
        {
            name:    "yes error",
            want:    4,
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := SomeFunction()
    
            test.WantErr(t, err, tt.wantErr)
            test.Equal(t, got, tt.want)
        })
    }
}

Which is basically semantically equivalent to:

func TestTableThings(t *testing.T) {
    tests := []struct {
        name    string
        want    int
        wantErr bool
    }{
        {
            name:    "no error",
            want:    4,
            wantErr: false,
        },
        {
            name:    "yes error",
            want:    4,
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := SomeFunction()
    
            if tt.wantErr {
                test.Err(t, err)
            } else {
                test.Ok(t, err)
            }
            test.Equal(t, got, tt.want)
        })
    }
}
Capturing Stdout and Stderr

We've all been there, trying to test a function that prints but doesn't accept an io.Writer as a destination 🙄.

That's where test.CaptureOutput comes in!

func TestOutput(t *testing.T) {
    // Function that prints to stdout and stderr, but imagine this is defined somewhere else
    // maybe a 3rd party library that you don't control, it just prints and you can't tell it where
    fn := func() error {
        fmt.Fprintln(os.Stdout, "hello stdout")
        fmt.Fprintln(os.Stderr, "hello stderr")

        return nil
    }

    // CaptureOutput to the rescue!
    stdout, stderr := test.CaptureOutput(t, fn)

    test.Equal(t, stdout, "hello stdout\n")
    test.Equal(t, stderr, "hello stderr\n")
}

Under the hood CaptureOutput temporarily captures both streams, copies the data to a buffer and returns the output back to you, before cleaning everything back up again.

See Also
Credits

This package was created with copier and the FollowTheProcess/go_copier project template.

Documentation

Overview

Package test provides a lightweight, but useful extension to the std lib's testing package with a friendlier and more intuitive API.

Simple tests become trivial and test provides mechanisms for adding useful context to test failures.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CaptureOutput added in v0.9.0

func CaptureOutput(tb testing.TB, fn func() error) (stdout, stderr string)

CaptureOutput captures and returns data printed to os.Stdout and os.Stderr by the provided function fn, allowing you to test functions that write to those streams and do not have an option to pass in an io.Writer.

If the provided function returns a non nil error, the test is failed with the error logged as the reason.

If any error occurs capturing stdout or stderr, the test will also be failed with a descriptive log.

fn := func() error {
	fmt.Println("hello stdout")
	return nil
}

stdout, stderr := test.CaptureOutput(t, fn)
fmt.Print(stdout) // "hello stdout\n"
fmt.Print(stderr) // ""

func Diff

func Diff(tb testing.TB, got, want string)

Diff fails if the two strings got and want are not equal and provides a rich unified diff of the two for easy comparison.

func DiffBytes added in v0.19.0

func DiffBytes(tb testing.TB, got, want []byte)

DiffBytes fails if the two []byte got and want are not equal and provides a rich unified diff of the two for easy comparison.

func Equal

func Equal[T comparable](tb testing.TB, got, want T, options ...Option)

Equal fails if got != want.

test.Equal(t, "apples", "apples") // Passes
test.Equal(t, "apples", "oranges") // Fails

func EqualFunc

func EqualFunc[T any](tb testing.TB, got, want T, equal func(a, b T) bool, options ...Option)

EqualFunc is like Equal but accepts a custom comparator function, useful when the items to be compared do not implement the comparable generic constraint.

The signature of the comparator is such that standard library functions such as slices.Equal or maps.Equal can be used.

The comparator should return true if the two items should be considered equal.

test.EqualFunc(t, []int{1, 2, 3}, []int{1, 2, 3}, slices.Equal) // Passes
test.EqualFunc(t, []int{1, 2, 3}, []int{4, 5, 6}, slices.Equal) // Fails

func Err added in v0.2.0

func Err(tb testing.TB, err error, options ...Option)

Err fails if err == nil.

err := shouldFail()
test.Err(t, err)

func False

func False(tb testing.TB, got bool, options ...Option)

False fails if got is true.

test.False(t, false) // Passes
test.False(t, true) // Fails

func NearlyEqual added in v0.8.0

func NearlyEqual[T ~float32 | ~float64](tb testing.TB, got, want T, options ...Option)

NearlyEqual is like Equal but for floating point numbers where absolute equality often fails.

If the difference between got and want is sufficiently small, they are considered equal. This threshold defaults to 1e-8 but can be configured with the FloatEqualityThreshold option.

test.NearlyEqual(t, 3.0000000001, 3.0) // Passes, close enough to be considered equal
test.NearlyEqual(t, 3.0000001, 3.0) // Fails, too different

func NotEqual

func NotEqual[T comparable](tb testing.TB, got, want T, options ...Option)

NotEqual is the opposite of Equal, it fails if got == want.

test.NotEqual(t, 10, 42) // Passes
test.NotEqual(t, 42, 42) // Fails

func NotEqualFunc

func NotEqualFunc[T any](tb testing.TB, got, want T, equal func(a, b T) bool, options ...Option)

NotEqualFunc is like Equal but accepts a custom comparator function, useful when the items to be compared do not implement the comparable generic constraint.

The signature of the comparator is such that standard library functions such as slices.Equal or maps.Equal can be used.

The comparator should return true if the two items should be considered equal.

test.EqualFunc(t, []int{1, 2, 3}, []int{1, 2, 3}, slices.Equal) // Fails
test.EqualFunc(t, []int{1, 2, 3}, []int{4, 5, 6}, slices.Equal) // Passes

func Ok

func Ok(tb testing.TB, err error, options ...Option)

Ok fails if err != nil.

err := doSomething()
test.Ok(t, err)

func True

func True(tb testing.TB, got bool, options ...Option)

True fails if got is false.

test.True(t, true) // Passes
test.True(t, false) // Fails

func WantErr added in v0.5.0

func WantErr(tb testing.TB, err error, want bool, options ...Option)

WantErr fails if you got an error and didn't want it, or if you didn't get an error but wanted one.

It greatly simplifies checking for errors in table driven tests where an error may or may not be nil on any given test case.

test.WantErr(t, errors.New("uh oh"), true) // Passes, got error when we wanted one
test.WantErr(t, errors.New("uh oh"), false) // Fails, got error but didn't want one
test.WantErr(t, nil, true) // Fails, wanted an error but didn't get one
test.WantErr(t, nil, false) // Passes, didn't want an error and didn't get one

Types

type Option added in v0.19.0

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

Option is a configuration option for a test.

func Context added in v0.19.0

func Context(format string, args ...any) Option

Context is an Option that allows the caller to inject useful contextual information as to why the test failed. This can be a useful addition to the test failure output log.

The signature of context allows the use of fmt print verbs to format the message in the same way one might use fmt.Sprintf.

It is not necessary to include a newline character at the end of format.

Setting context explicitly to the empty string "" is an error and will fail the test.

For example:

err := doSomethingComplicated()
test.Ok(t, err, test.Context("something complicated failed"))

func FloatEqualityThreshold added in v0.19.0

func FloatEqualityThreshold(threshold float64) Option

FloatEqualityThreshold is an Option to set the maximum difference allowed between two floating point numbers before they are considered equal. This setting is only used in NearlyEqual and [NotNearlyEqual].

Setting threshold to ±math.Inf is an error and will fail the test.

The default is 1e-8, a sensible default for most cases.

func Title added in v0.19.0

func Title(title string) Option

Title is an Option that sets the title of the test in the test failure log.

The title is shown as an underlined header in the test failure, below which the actual and expected values will be shown.

By default this will be named sensibly after the test function being called, for example Equal has a default title "Not Equal".

Setting title explicitly to the empty string "" is an error and will fail the test.

test.Equal(t, "apples", "oranges", test.Title("Wrong fruits!"))

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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