tutl

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: May 16, 2023 License: Unlicense Imports: 10 Imported by: 2

README

go-tutl

go-tutl is a Tiny Unit Testing Library (for Go). "Tutl" is also the Faroese word for "whisper", a nod to it's light-weight nature.

TUTL provides a few helper routines that make simple unit testing in Go much easier and that encourage you to write tests that, when they fail, it is easy to figure out what broke.

It started as the 2 small functions that I have felt worth writing every time I needed to do unit tests in Go, Is() and S(). Plus 2 other small functions that I usually eventually end up writing when I get further along with a project, Like() and ShowStackOnInterrupt(). And it has been collecting small improvements over the years.

Example usage:

package duration
import (
    "testing"

    u "github.com/Unity-Technologies/go-tutl-internal"
    _ "github.com/Unity-Technologies/go-tutl-internal/hang" // ^C gives stack dumps.
)

func TestDur(t *testing.T) {
    u.Is("1m 1s", DurationAsText(61), "61", t)

    got, err := TextAsDuration("1h 5s")
    u.Is(nil, err, "Error from '1h 5s'", t)
    u.Is(60*60+5, got, "'1h 5s'", t)

    got, err = TextAsDuration("3 fortnight")
    u.Like(err, "Error from '3 fortnight'", t,
        "(Unknown|Invalid) unit", "*fortnight")
}

Sample output from a failing run of the above tests:

dur_test.go:10: Got "1m 61s" not "1m 1s" for 61.
dur_test.go:14: Got 3600 not 3605 for '1h 5s'.
dur_test.go:17:
    Not like /(Unknown|Invalid) unit/...
    and No <fortnight>...
    In <"Bad unit (ortnight) in duration.
    "> for Error from '3 fortnight'.

It also provides a special module to deal with infinite loops in your code. If you include:

import (
    _ "github.com/Unity-Technologies/go-tutl-internal/hang" // ^C gives stack dumps.
)

in just one of your *_test.go files, then you can interrupt (such as via typing Ctrl-C) an infinite loop or otherwise hanging test run and be shown, in response, the stack traces of everything that is running.

Documentation

Overview

go-tutl is a Trivial Unit Testing Library (for Go). Example usage:

package duration
import (
	"testing"

	u "github.com/Unity-Technologies/go-tutl-internal"
	_ "github.com/Unity-Technologies/go-tutl-internal/hang" // ^C gives stack dumps.
)

func TestDur(t *testing.T) {
	u.Is("1m 1s", DurationAsText(61), "61", t)

	got, err := TextAsDuration("1h 5s")
	u.Is(nil, err, "Error from '1h 5s'", t)
	u.Is(60*60+5, got, "'1h 5s'", t)

	got, err = TextAsDuration("3 fortnight")
	u.Like(err, "Error from '3 fortnight'", t,
		"(Unknown|Invalid) unit", "*fortnight")
}

See also "go doc github.com/Unity-Technologies/go-tutl-internal/hang".

Index

Constants

View Source
const MaxDigits32 = 7
View Source
const MaxDigits64 = 15

Variables

View Source
var Default = Options{
	LineWidth: 72, PathLength: 20, Digits32: 5, Digits64: 12,
	// contains filtered or unexported fields
}

The 'tutl.Default' global contains the user preferences to be used unless you make a copy and use it, such as via New() (see Options for more).

View Source
var StdoutTester = FakeTester{os.Stdout, false}

The 'tutl.StdoutTester' is a replacement for a '*testing.T' that just writes output to 'os.Stdout'.

Functions

func AtInterrupt

func AtInterrupt(f func()) func()

AtInterrupt registers a function to be called if the test run is interrupted (by the user typing Ctrl-C or whatever sends SIGINT). The function registered first is run last. You must have called ShowStackOnInterrupt() or AtInterrupt() will do nothing useful.

The function passed in is also returned. This can be useful for clean-up code that should be run whether the test run is interrupted or not:

defer tutl.AtInterrupt(cleanup)()

If you want to empower AtInterrupt() even if ShowStackOnInterrupt() has not been enabled, then your code can call:

go tutl.ShowStackOnInterrupt(false)

This does nothing if ShowStackOnInterrupt() had already been called (in particular, it does not disable the showing of stack traces). Otherwise, it does the work that it would normally do except, if a SIGINT is received, it will only run any functions registered via AtInterrupt() but will not show stack traces.

func Char

func Char(c byte) string

Char(c) is similar to Rune(rune(c)), except it escapes all byte values of 0x80 and above into 6-character strings like '\x9B' (rather then converting them UTF-8).

func Circa added in v1.2.0

func Circa(digits int, want, got float64, desc string, t TestingT) bool

Circa() tests that the 2nd and 3rd arguments are approximately equal to each other. If they are not, then a diagnostic is displayed which also causes the unit test to fail.

The diagnostic is similar to "Got {got} not {want} for {desc}.\n" where 'want' and 'got' are shown formatted via 'fmt.Sprintf("%.*g", digits, v)'. They are considered equal if that formatting produces the same string for both values. That is, 'want' and 'got' are considered roughly equal if they are the same to 'digits' significant digits. Passing 'digits' as less than 1 or more than 15 is not useful.

Circa() returns whether the test passed, which is useful for skipping tests that would make no sense to run given a prior failure or to display extra debug information only when a test fails.

func DoubleQuote

func DoubleQuote(s string) string

DoubleQuote() returns the string enclosed in double quotes and with contained \ and " characters escaped.

func Escape

func Escape(r rune) string

Escape() returns a string containing the passed-in rune, unless it is a control character. Runes '\n', '\r', and '\t' each return a 2-character string (\n, \r, or \t). Other 7-bit control characters are turned into strings like \x1B. The 8-bit control characters are turned into strings like \u009B. EscapeNewline(false) does not affect Escape().

func EscapeNewline

func EscapeNewline(b bool)

After calling EscapeNewline(true), S() will escape '\n' characters. You can call EscapeNewline(false) to restore the default behavior.

func GetPanic added in v1.2.0

func GetPanic(run func()) (failure interface{})

GetPanic() calls the passed-in function and returns 'nil' or the argument that gets passed to panic() from within it. This can be used in other test functions, for example:

u := tutl.New(t)
u.Is(nil, u.GetPanic(func(){ obj.Method(nil) }), "Method panic")

func HasType added in v1.2.0

func HasType(want string, got interface{}, desc string, t TestingT) bool

HasType() tests that the type of the 2nd argument ('got') is equal to the first argument ('want', a string). That is, it checks that 'want == fmt.Sprintf("%T", got)'. If not, then a diagnostic is displayed which also causes the unit test to fail.

The diagnostic is similar to "Got {got} not {want} for {desc}.\n" where '{got}' is the data type of 'got' and '{want}' is just the 'want' string.

If 'got' is an 'interface' type, then the type string will be the type of the underlying object (or "nil"). If you actually wish to compare the 'interface' type, then place '&' before 'got' and prepend "*" to 'want':

got := GetReader() // Returns io.Reader interface to an *os.File
tutl.HasType("*os.File", got, "underlying type is *os.File", t)
tutl.HasType("*io.Reader", &got, "interface type is io.Reader", t)
//            ^            ^ insert these to test interface type

HasType() returns whether the test passed, which is useful for skipping tests that would make no sense to run given a prior failure.

func Is

func Is(want, got interface{}, desc string, t TestingT) bool

Is() tests that the first two arguments are converted to the same string by V(). If they are not, then a diagnostic is displayed which also causes the unit test to fail.

The diagnostic is similar to "Got {got} not {want} for {desc}.\n" except that; 1) S() is used for 'got' and 'want' so control characters will be escaped and their values may be in quotes and 2) it will be split onto multiple lines if the values involved are long enough.

Note that you pass 'want' before 'got' when calling Is() because the 'want' value is often a simple constant while 'got' can be a complex call and code is easier to read if you put shorter things first. But the output shows 'got' before 'want' as "Got X not Y" is the shortest way to express that concept in English.

Is() returns whether the test passed, which is useful for skipping tests that would make no sense to run given a prior failure or to display extra debug information only when a test fails.

func IsNot

func IsNot(hate, got interface{}, desc string, t TestingT) bool

IsNot() tests that the first two arguments are converted to different strings by V(). If they are not, then a diagnostic is displayed which also causes the unit test to fail. The diagnostic is similar to "Got unwanted {got} for {desc}.\n" except that S() is used for 'got' so control characters will be escaped and their values may be in quotes.

IsNot() returns whether the test passed, which is useful for skipping tests that would make no sense to run given a prior failure.

func Like

func Like(got interface{}, desc string, t TestingT, match ...string) int

Like() is most often used to test error messages (or other complex strings). It lets you perform multiple tests against a single value. Each test checks that the value converts into a string that either contains a specific sub-string (ignoring letter case) or that it matches a regular expression. You must pass at least one string to be matched.

Strings that start with "*" have the "*" stripped before a substring match is performed (ignoring letter case). If a string does not start with a "*", then it must be a valid regular expression that will be matched against the value's string representation.

Except that strings that start with "!" have that stripped before checking for a subsequent "*". The "!" negates the match so that the test will only pass if the string does not match. To specify a regular expression that starts with a "!" character, simply escape it as `\!` or "[!]".

Like() returns the number of matches that failed.

If 'got' is 'nil', the empty string, or becomes the empty string, then no comparisons are done and a single failure is reported (but the number returned is the number of match strings as it is assumed that none of them would have matched the empty string).

func ReplaceNewlines added in v1.2.0

func ReplaceNewlines(s string) string

ReplaceNewlines() returns a string with each newline replaced with either an escaped newline (a \ then an 'n') or with the string "\n...." (so that subsequent lines of a multi-line value are indented to make them easier to distinguish from subsequent lines of a test diagnostic).

func Rune

func Rune(r rune) string

Rune() returns a string consisting of the rune enclosed in single quotes, except that control characters are escaped [see Escape()].

Note that neither ' nor \ characters are escaped so Char('\”) returns "”'" (3 apostrophes) and Char('\\') returns `'\'` (partly because `'\”` and `'\\'` are rather ugly).

func S

func S(vs ...interface{}) string

S() returns a single string composed by converting each argument into a string and concatenating all of those strings. It is similar to but not identical to 'fmt.Sprint()'. S() never inserts spaces between your values (if you want spaces, it is easy for you to add them). S() puts single quotes around 'byte' (and 'uint8') values. S() treats '[]byte' values like 'string's. S() puts double quotes around '[]byte' and 'error' values (escaping enclosed " and \ characters).

S() escapes control characters except for newlines [but see EscapeNewline()]. S() also escapes non-UTF-8 byte sequences.

If S() is passed a single argument that is a 'string', then it will put double quotes around it and escape any contained " and \ characters.

See V() for how 'float32', 'float64', '[]float32', or '[]float64' values are converted.

Note that S() does not put single quotes around 'rune' values as 'rune' is just an alias for 'int32' so S('x') == S(int32('x')) == "120" while S("x"[0]) == S(byte('x')) == S(uint8('x')) = "'x'".

func ShowStackOnInterrupt

func ShowStackOnInterrupt(show ...bool)

If you have a TestMain() function, then you can add

go tutl.ShowStackOnInterrupt()

to it to allow you to interrupt it (such as via typing Ctrl-C) in order to see stack traces of everything that is running. This is particularly useful if your code has an infinite loop.

See also "go doc github.com/Unity-Technologies/go-tutl-internal/hang".

ShowStackOnInterrupt() can also be used from non-test programs.

ShowStackOnInterrupt(false) has a special meaning; see AtInterrupt().

func V

func V(v interface{}) string

V() just converts a value to a string. It is similar to 'fmt.Sprint(v)'. But it treats '[]byte' values as 'string's. It also (by default) uses fewer significant digits when converting 'float32', 'float64', '[]float32', and '[]float64' values (see Options for details).

Types

type FakeTester added in v1.2.0

type FakeTester struct {
	Output    io.Writer
	HasFailed bool
}

A FakeTester is a replacement for a '*testing.T' so that you can use TUTL's functionality outside of a real 'go test' run.

func (FakeTester) Error added in v1.2.0

func (out FakeTester) Error(args ...interface{})

func (FakeTester) Errorf added in v1.2.0

func (out FakeTester) Errorf(format string, args ...interface{})

func (FakeTester) Failed added in v1.2.0

func (out FakeTester) Failed() bool

func (FakeTester) Helper added in v1.2.0

func (out FakeTester) Helper()

func (FakeTester) Log added in v1.2.0

func (out FakeTester) Log(args ...interface{})

func (FakeTester) Logf added in v1.2.0

func (out FakeTester) Logf(format string, args ...interface{})

type Options added in v1.2.0

type Options struct {

	// LineWidth influences when "Got {got} not {want} for {title}" output
	// gets split onto multiple lines instead.  If that string is longer
	// than LineWidth, then it gets split into "Got ...\nnot ...\n...".
	//
	// This also happens if you aren't escaping newlines and either value
	// contains a newline (and the newlines get indentation added so that
	// the output is easier to understand).
	//
	// If the diagnostic line is no longer than LineWidth but is longer than
	// LineWidth-PathLength, then a newline gets prepended to it as the
	// prepended source info would likely cause the diagnostic to wrap.
	//
	LineWidth int

	// PathLength is the maximum expected length of the path to the
	// *_test.go file being run plus the line number that 'go test'
	// prepends to each diagnostic.  It defaults to 20.
	//
	PathLength int

	// Digits32 specifies how many significant digits to use when comparing
	// 'float32' values.  In particular, if a 'float32' or '[]float32' value
	// is passed to V(), then no more than Digits32 significant digits are
	// used in the resulting string.  Other data structures that contain
	// 'float32' values are not impacted.
	//
	// If Digits32 is 0, then the default value of 5 is used.  If Digits32
	// is negative or more than 7, then 'fmt.Sprint()' is used which may
	// use even 8 digits for some values (such as 1/3) so that 2 'float32'
	// values that are even very slightly different will produce different
	// strings (a 'float32' is accurate to only slightly more than 7 digits).
	//
	Digits32 int

	// Digits64 specifies how many significant digits to use when comparing
	// 'float64' values.  In particular, if a 'float64' or '[]float64' value
	// is passed to V(), then no more than Digits64 significant digits are
	// used in the resulting string.  Other data structures that contain
	// 'float64' values are not impacted.
	//
	// If Digits64 is 0, then the default value of 12 is used.  If Digits64
	// is negative or more than 16, then 'fmt.Sprint()' is used which may
	// use up to 16 digits so that 2 'float64' values that are even very
	// slightly different will produce different strings (a 'float64' is
	// accurate to only slightly less than 16 digits).
	//
	Digits64 int
	// contains filtered or unexported fields
}

Options contains user preference options. The 'tutl.Default' global is the Options used unless you make a copy of it and use the copy.

Calling tutl.New(t) associates such a copy with the returned object so changes to preferences via the returned object don't modify 'Default'.

func TestFoo(t *testing.T) {
    u := tutl.New(t)
    u.SetLineWidth(120)
    u.Is(want, got(), "") // Uses 120-character line width.
    tutl.Is(want, got(), "", t) // Uses tutl.Default's line width.
}

You can modify some options directly via 'tutl.Default', such as:

tutl.Default.LineWidth = 120

func (Options) Circa added in v1.2.0

func (o Options) Circa(
	digits int, want, got float64, desc string, t TestingT,
) bool

See tutl.Circa() for documentation.

func (*Options) EscapeNewline added in v1.2.0

func (o *Options) EscapeNewline(b bool)

See tutl.EscapeNewline() for documentation.

func (Options) HasType added in v1.2.0

func (o Options) HasType(
	want string, got interface{}, desc string, t TestingT,
) bool

See tutl.HasType() for documentation.

func (Options) Is added in v1.2.0

func (o Options) Is(want, got interface{}, desc string, t TestingT) bool

See tutl.Is() for documentation.

func (Options) IsNot added in v1.2.0

func (o Options) IsNot(hate, got interface{}, desc string, t TestingT) bool

See tutl.IsNot() for documentation.

func (Options) Like added in v1.2.0

func (o Options) Like(
	got interface{}, desc string, t TestingT, match ...string,
) int

See tutl.Like() for documentation.

func (*Options) ReplaceNewlines added in v1.2.0

func (o *Options) ReplaceNewlines(s string) string

See tutl.ReplaceNewlines() for documentation.

func (Options) S added in v1.2.0

func (o Options) S(vs ...interface{}) string

See tutl.S() for documentation.

func (Options) V added in v1.2.0

func (o Options) V(v interface{}) string

See tutl.V() for documentation.

type TUTL

type TUTL struct {
	TestingT
	// contains filtered or unexported fields
}

TUTL is a type used to allow an alternate calling style, especially for Is() and Like().

func New

func New(t TestingT) TUTL

A unit test can have a huge number of calls to Is(). Having to remember to pass in the *testing.T argument can be inconvenient. TUTL offers an alternate calling method that replaces the huge number of such extra arguments with a single line of code. This example code:

import (
    "testing"
    u "github.com/Unity-Technologies/go-tutl-internal"
    ^^  Import alias
)

func TestDur(t *testing.T) {
    u.Is("1m 1s", DurationAsText(61), "61", t)
    //                                    ^^^  Extra argument
    u.Like(Valid("3f", "Error from '3f'", t, "(Unknown|Invalid) unit")
    //                                    ^^^  Extra argument
}

would become:

import (
    "testing"
    "github.com/Unity-Technologies/go-tutl-internal"
)

func TestDur(t *testing.T) {
    var u = tutl.New(t)
    // ^^^^^^^^^^^^^^^^ Added line
    u.Is("1m 1s", DurationAsText(61), "61")
    u.Like(Valid("3f", "Error from '3f'", "(Unknown|Invalid) unit")
}

Whether to use an import alias or New() (or neither) is mostly a personal preference. Though, using New() also limits the scope of EscapeNewline() and other options.

New() also copies the current settings from the global 'tutl.Default' into the returned object.

func (TUTL) Char

func (u TUTL) Char(c byte) string

Identical to the non-method tutl.Char().

func (TUTL) Circa added in v1.2.0

func (u TUTL) Circa(digits int, want, got float64, desc string) bool

Same as the non-method tutl.Circa() except the '*testing.T' argument is held in the TUTL object and so does not need to be passed as an argument.

func (TUTL) DoubleQuote

func (u TUTL) DoubleQuote(s string) string

Identical to the non-method tutl.DoubleQuote().

func (TUTL) Escape

func (u TUTL) Escape(r rune) string

Identical to the non-method tutl.Escape().

func (*TUTL) EscapeNewline

func (u *TUTL) EscapeNewline(b bool)

Same as the EscapeNewline() method on the 'tutl.Default' global, except it only changes the setting for the invoking TUTL object.

func (TUTL) GetPanic added in v1.2.0

func (_ TUTL) GetPanic(run func()) interface{}

GetPanic() calls the passed-in function and returns 'nil' or the argument that gets passed to panic() from within it. This can be used in other test functions, for example:

u.Is(nil, u.GetPanic(func(){ obj.Method(nil) }), "Method panic")

func (TUTL) HasType added in v1.2.0

func (u TUTL) HasType(want string, got interface{}, desc string) bool

Same as the non-method tutl.HasType() except the '*testing.T' argument is held in the TUTL object and so does not need to be passed as an argument.

func (TUTL) Is

func (u TUTL) Is(want, got interface{}, desc string) bool

Same as the non-method tutl.Is() except the '*testing.T' argument is held in the TUTL object and so does not need to be passed as an argument.

func (TUTL) IsNot

func (u TUTL) IsNot(hate, got interface{}, desc string) bool

Same as the non-method tutl.IsNot() except the '*testing.T' argument is held in the TUTL object and so does not need to be passed as an argument.

func (TUTL) Like

func (u TUTL) Like(got interface{}, desc string, match ...string) int

Same as the non-method tutl.Like() except the '*testing.T' argument is held in the TUTL object and so does not need to be passed as an argument.

func (*TUTL) ReplaceNewlines added in v1.2.0

func (u *TUTL) ReplaceNewlines(s string) string

Same as the ReplaceNewlines() method on the 'tutl.Default' global, except it honors the settings from the invoking TUTL object.

func (TUTL) Rune

func (u TUTL) Rune(r rune) string

Identical to the non-method tutl.Rune().

func (TUTL) S

func (u TUTL) S(vs ...interface{}) string

Same as the non-method tutl.S() except that it honors the option settings of the invoking TUTL object, not of the 'tutl.Default' global.

func (TUTL) SetDigits32 added in v1.2.0

func (u TUTL) SetDigits32(d int)

SetDigits32() is the same as setting the global 'tutl.Default.Digits32' value, except it only changes the setting for the invoking TUTL object.

func (TUTL) SetDigits64 added in v1.2.0

func (u TUTL) SetDigits64(d int)

SetDigits64() is the same as setting the global 'tutl.Default.Digits64' value, except it only changes the setting for the invoking TUTL object.

func (*TUTL) SetLineWidth

func (u *TUTL) SetLineWidth(w int)

SetLineWidth() is the same as setting the global 'tutl.Default.LineWidth' except it only changes the setting for the invoking TUTL object.

func (*TUTL) SetPathLength added in v1.2.0

func (u *TUTL) SetPathLength(l int)

SetPathLength() is the same as setting the global 'tutl.Default.PathLength' except it only changes the setting for the invoking TUTL object.

func (TUTL) V

func (u TUTL) V(v interface{}) string

Same as the non-method tutl.V() except that it honors the option settings of the invoking TUTL object, not of the tutl.Default global.

type TestingT

type TestingT interface {
	Helper()
	Error(args ...interface{})
	Errorf(format string, args ...interface{})
	Log(args ...interface{})
	Logf(format string, args ...interface{})
	Failed() bool
}

TestingT is an interface covering the methods of '*testing.T' that TUTL uses. This makes it easier to test this test library.

Directories

Path Synopsis
Include:
Include:
A couple of simple helper functions to make it easy to get CPU or blocking profile data from your tests.
A couple of simple helper functions to make it easy to get CPU or blocking profile data from your tests.

Jump to

Keyboard shortcuts

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