Git Product home page Git Product logo

test's Introduction

test

Go Reference MPL License Run CI Tests

test is a modern and generics oriented testing assertions library for Go.

There are five key packages,

  • must - assertions causing test failure and halt the test case immediately
  • test - assertions causing test failure and allow the test case to continue
  • wait - utilities for waiting on conditionals in tests
  • skip - utilities for skipping test cases in some situations
  • portal - utilities for allocating free ports for network listeners in tests

Changes

  • v1.8.1 adds the function type ErrorAssertionFunc that is useful for passing Error or NoError in table driven tests

☑️ v1.8.0 introduces the skip package for skipping tests!

  • New helper functions for skipping out tests based on some given criteria

☑️ v1.7.0 marks the first stable release!

  • Going forward no breaking changes will be made without a v2 major version

☑️ v0.6.0 adds support for custom cmp.Option values

  • Adds ability to customize cmp.Equal behavior via cmp.Option arguments
  • Adds assertions for existence of single map key
  • Fixes some error outputs

Requirements

Only depends on github.com/google/go-cmp.

The minimum Go version is go1.18.

Install

Use go get to grab the latest version of test.

go get -u github.com/shoenig/test@latest

Influence

This library was made after a ~decade of using testify, quite possibly the most used library in the whole Go ecosystem. All credit of inspiration belongs them.

Philosophy

Go has always lacked a strong definition of equivalency, and until recently lacked the language features necessary to make type-safe yet generic assertive statements based on the contents of values.

This test (and companion must) package aims to provide a test-case assertion library where the caller is in control of how types are compared, and to do so in a strongly typed way - avoiding erroneous comparisons in the first place.

Generally there are 4 ways of asserting equivalence between types.

the == operator

Functions like EqOp and ContainsOp work on types that are comparable, i.e., are compatible with Go's built-in == and != operators.

a comparator function

Functions like EqFunc and ContainsFunc work on any type, as the caller passes in a function that takes two arguments of that type, returning a boolean indicating equivalence.

an .Equal method

Functions like Equal and ContainsEqual work on types implementing the EqualFunc generic interface (i.e. implement an .Equal method). The .Equal method is called to determine equivalence.

the cmp.Equal or reflect.DeepEqual functions

Functions like Eq and Contains work on any type, using the cmp.Equal or reflect.DeepEqual functions to determine equivalence. Although this is the easiest / most compatible way to "just compare stuff", it's the least deterministic way of comparing instances of a type. Changes to the underlying types may cause unexpected changes in their equivalence (e.g., the addition of unexported fields, function field types, etc.). Assertions that make use of cmp.Equal configured with custom cmp.Option values.

output

When possible, a nice diff output is created to show why an equivalence has failed. This is done via the cmp.Diff function. For incompatible types, their GoString values are printed instead.

All output is directed through t.Log functions, and is visible only if test verbosity is turned on (e.g., go test -v).

fail fast vs. fail later

The test and must packages are identical, except for how test cases behave when encountering a failure. Sometimes it is helpful for a test case to continue running even though a failure has occurred (e.g., it contains cleanup logic not captured via a t.Cleanup function). Other times, it makes sense to fail immediately and stop the test case execution.

go-cmp Options

The test assertions that rely on cmp.Equal can be customized in how objects are compared by specifying custom cmp.Option values. These can be configured through test.Cmp and must.Cmp helpers. Google provides some common custom behaviors in the cmpopts package. The protocmp package is also particularly helpful when working with Protobuf types.

Here is an example of comparing two slices, but using a custom Option to sort the slices so that the order of elements does not matter.

a := []int{3, 5, 1, 6, 7}
b := []int{1, 7, 6, 3, 5}
must.Eq(t, a, b, must.Cmp(cmpopts.SortSlices(func(i, j int) bool {
  return i < j
})))

PostScripts

Some tests are large and complex (like e2e testing). It can be helpful to provide more context on test case failures beyond the actual assertion. Logging could do this, but often we want to only produce output on failure.

The test and must packages provide a PostScript interface which can be implemented to add more context in the output of failed tests. There are handy implementations of the PostScript interface provided - Sprint, Sprintf, Values, and Func.

By adding one or more PostScript to an assertion, on failure the error message will be appended with the additional context.

// Add a single Sprintf-string to the output of a failed test assertion.
must.Eq(t, exp, result, must.Sprintf("some more context: %v", value))
// Add a formatted key-value map to the output of a failed test assertion.
must.Eq(t, exp, result, must.Values(
  "one", 1,
  "two", 2,
  "fruit", "banana",
))
// Add the output from a closure to the output of a failed test assertion.
must.Eq(t, exp, result, must.Func(func() string {
  // ... something interesting
  return s
})

Skip

Sometimes it makes sense to just skip running a certain test case. Maybe the operating system is incompatible or a certain required command is not installed. The skip package provides utilities for skipping tests under some given conditions.

skip.OperatingSystem(t, "windows", "plan9", "dragonfly")
skip.NotArchitecture(t, "amd64", "arm64")
skip.CommandUnavailable(t, "java")
skip.EnvironmentVariableSet(t, "CI")

Wait

Sometimes a test needs to wait on a condition for a non-deterministic amount of time. For these cases, the wait package provides utilities for configuring conditionals that can assert some condition becomes true, or that some condition remains true - whether for a specified amount time, or a specific number of iterations.

A Constraint is created in one of two forms

  • InitialSuccess - assert a function eventually returns a positive result
  • ContinualSuccess - assert a function continually returns a positive result

A Constraint may be configured with a few Option functions.

  • Timeout - set a time bound on the constraint
  • Attempts - set an iteration bound on the constraint
  • Gap - set the iteration interval pace
  • BoolFunc - set a predicate function of type func() bool
  • ErrorFunc - set a predicate function of type func() error
  • TestFunc - set a predicate function of type func() (bool, error)

Assertions form

The test and must package implement an assertion helper for using the wait package.

must.Wait(t, wait.InitialSuccess(wait.ErrorFunc(f)))
must.Wait(t, wait.ContinualSuccess(
    wait.ErrorFunc(f),
    wait.Attempts(100),
    wait.Gap(10 * time.Millisecond),
))

Fundamental form

Although the 99% use case is via the test or must packages as described above, the wait package can also be used in isolation by calling Run() directly. An error is returned if the conditional failed, and nil otherwise.

c := wait.InitialSuccess(
    BoolFunc(f),
    Timeout(10 * time.Seconds),
    Gap(1 * time.Second),
)
err := c.Run()

Examples (equality)

import "github.com/shoenig/test/must"

// ...

e1 := Employee{ID: 100, Name: "Alice"}
e2 := Employee{ID: 101, Name: "Bob"}

// using cmp.Equal (like magic!)
must.Eq(t, e1, e2)

// using == operator
must.EqOp(t, e1, e2)

// using a custom comparator
must.EqFunc(t, e1, e2, func(a, b *Employee) bool {
    return a.ID == b.ID
})

// using .Equal method
must.Equal(t, e1, e2)

Output

The test and must package attempt to create useful, readable output when an assertion goes awry. Some random examples below.

test_test.go:779: expected different file permissions
↪ name: find
↪ exp: -rw-rwx-wx
↪ got: -rwxr-xr-x
tests_test.go:569: expected maps of same values via 'eq' function
↪ difference:
map[int]test.Person{
0: {ID: 100, Name: "Alice"},
  	1: {
  		ID:   101,
-  		Name: "Bob",
+  		Name: "Bob B.",
    	},
    }
test_test.go:520: expected slice[1].Less(slice[2])
↪ slice[1]: &{200 Bob}
↪ slice[2]: &{150 Carl}
test_test.go:688: expected maps of same values via .Equal method
↪ differential ↷
  map[int]*test.Person{
  	0: &{ID: 100, Name: "Alice"},
  	1: &{
- 		ID:   101,
+ 		ID:   200,
  		Name: "Bob",
  	},
  }
test_test.go:801: expected regexp match
↪ s: abcX
↪ re: abc\d

License

Open source under the MPL

test's People

Contributors

alessio-perugini avatar amirkhaki avatar ccarstens avatar ccoveille avatar dependabot[bot] avatar gulducat avatar johanbrandhorst avatar pkazmierczak avatar shoenig avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

test's Issues

idea: test helper for localhost

Sometimes we write code that encapsulates resolving localhost and the output of that code contains the resolved localhost address - which tends to vary depending on if/how IPv6 is configured. One environment may produce 127.0.0.1 while another [::1].

A helper like Localhost(t *testing.T, s string) would be the most basic form, but there also tends to be cases where we want to include a port, and some cases with a protocol.

Provide colored diffing

Is your feature request related to a problem? Please describe.
It would be great to have colors in diffs, to make it easier to spot where issues are.

Describe the solution you'd like
Provide an option to enable colors, disabled by default to be backwards compatible.

Additional context
See google/go-cmp#230 (comment)

Add a CI via GitHub actions

Is your feature request related to a problem? Please describe.
I pushed changes with #158 and I forgot to launch the tests
There was no CI to catch it.

We shouldn't be able to push code that could be merged without being tested.

Describe the solution you'd like
please add a GitHub action to launch the test to validate incoming PRs

idea: add retry / wait until logic

There's a retry package floating around that gets copied a lot. Would be nice to have its functionality just built into test framework.

using FileMode on directory does not work

    assert.go:14:
        task_dir_test.go:128: expected different file permissions
        ↪ name: tmp/TestTaskDir_NonRoot_Unveil1935793088/001/test-web/task
        ↪  exp: -rwx--x---
        ↪  got: drwx--x---

Maybe just have DirMode equivalents of FileMode, and that way we are asserting whether the thing is a file or a directory in addition to the mode.

Error funcs should check for nil error

The err passed in may be unexpectedly nil
e.g.

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0xa9dfaf]

goroutine 19 [running]:
testing.tRunner.func1.2({0xc5bd80, 0x132f5f0})
	/opt/google/go/src/testing/testing.go:1389 +0x24e
testing.tRunner.func1()
	/opt/google/go/src/testing/testing.go:1392 +0x39f
panic({0xc5bd80, 0x132f5f0})
	/opt/google/go/src/runtime/panic.go:838 +0x207
github.com/shoenig/test/internal/assertions.EqError({0x0?, 0x0?}, {0xd47273, 0xd})
	/home/shoenig/.go/pkg/mod/github.com/shoenig/[email protected]/internal/assertions/assertions.go:113 +0x2f
github.com/shoenig/test/must.EqError({0xe85900, 0xc00047c1a0}, {0x0, 0x0}, {0xd47273, 0xd})
	/home/shoenig/.go/pkg/mod/github.com/shoenig/[email protected]/must/must.go:47 +0x54
github.com/hashicorp/nomad/nomad/structs.TestServiceCheck_validateNomad.func1(0x0?)
	/home/shoenig/Work/go/nomad/nomad/structs/services_test.go:217 +0x75

idea: able to use Setting to skip assertions

sometimes i have a bunch of assertions and 1 of them is linux specific, e.g.

must.Eq(t, subcommands.ExitFailure, rc)
must.Eq(t, "", a.String())
must.Eq(t, "envy: failed to exec: fork/exec /does/not/exist: no such file or directory\n", b.String())
must.Eq(t, "", c.String())
must.Eq(t, "", d.String())

the exec failure string is specific to OS and fails on windows ... would be nice to just Skip that one test on Windows without lots of extra nonsense.

maybe something like

must.Eq(t, exp, val, must.RequiresOS("linux", "darwin"))

bug: newline on post script formatting

should be newline before ps

        |> running test case: 7 mixed file
--- FAIL: TestLocker_reads (0.00s)
    interface.go:24:
        landlock_linux_test.go:164: expected nil error
        ↪ error: landlock failed to lock: invalid argument↪ PostScript | annotation ↷
                paths [(rwc:file:tests/veggies/corn.txt) (rw:file:tests/fruits/apple.txt)]
FAIL

build: The make generate assumes gnu sed

From #155 (comment)

It would be nice if make generate worked whether the user was on Linux or macOS (and by extension the other BSD's).

I'm thinking of two ways to fix this. The simple thing would be to replace direct invocations of sed with a helper function, the implementation of which does the Right Thing depending on the version of sed.

And the other option would be to just replace generate.sh altogether with maybe a simple Perl or Python program that runs a function on the list of files we modify.

v0.5: fixup Contains parameters order

In a few cases like StrContains, the expectation comes after the value which is backwards from most other assertion styles. Would be nice to get these consistent.

regex: line up start of regex and string value

e.g., the 0:: should be lined up to make visual comparison easier

    basic_test.go:160: expected regexp match
    ↪ regex: 0::/nomad\.slice/5c12cd19\.+\.cat\.scope
    ↪ string: 0::/nomad.slice/5c12cd19-ca09-5d89-fda0-68516ebb54af.cat.scope
basic_test.go:35: RUN command: nomad args: [system gc]

idea: built-in interpolations in string / error comparisons

Kind of a general form of #19 - what if we had builtin interpolations available, e.g. ${test.hostname}, so you could write test cases like

must.EqError(t, "failed to connect to ${test.localhost}") 

which would automagically work across IPv4/IPv6 systems

idea: slice contains all

currently must.Contains only accepts a scalar, would be nice for a variant that lets us assert a slice contains a subslice

bug: SliceContainsFunc should be more flexible on types

Currently SliceContainsFunc asserts the given array and expected object are of the same type T, and the comparison function is (a, b T) bool. This prevents us from using the comparison func to extract a different type to compare (i.e. a particular field of a struct).

ideas: single value variants of MapContainsValue(s) helpers

Basically just single Value versions of each of

462:func MapContainsValues[M ~map[K]V, K comparable, V any](t T, m M, vals []V, settings ...Setting) {
474:func MapContainsValuesFunc[M ~map[K]V, K comparable, V any](t T, m M, vals []V, eq func(V, V) bool, settings ...Setting) {
486:func MapContainsValuesEqual[M ~map[K]V, K comparable, V interfaces.EqualFunc[V]](t T, m M, vals []V, settings ...Setting) {

so that we don't need to pass in a slice of values if we're just looking for one. And fix the function signature for the Func variant like on #134

Recommend swapping the naming convention for Eq and EqOp

Eq is nice and short... why not reserve that for comparable values, and make the reflect-based version the longer of the two? I think you'll end up with people using .Eq for everything, and lose the benefit of using generics entirely.

idea: be able to set a cleanup function on a wait constraint

let wait.On accept a func() that gets executed on failure

e.g.

s := TestServer 
// ... 
must.Wait(s.t, wait.On(
  wait.ErrorFunc(f),
  wait.Timeout(10*time.Second),
  wait.Gap(1*time.Second),
  wait.CleanupFailure(s.Stop), // new
), must.Sprint("failed to wait for leader"))

helper method for checking value of 1

We have a convenience helper for checking if a value is 0 (.Zero). Finding we also have a bunch of tests that return a status code where we expect a value of 1 - so how about a similar helper for those cases? (.One)

idea: helper for auto cleanup of tempfile

There is t.TempDir which creates a tmp directory and cleans itself up at the end of the test. It would be nice if there was something similar for a single file.

var filename string
must.TmpFile(t, "content", &filename) 

FileNotExist does not work

FileNotExist does not work as expected, due to misusing the fs package, which does not expect absolute paths. When you do, you get invalid argument error.

idea: adding additional context to tests

One feature left off is support of the msgAndArgs ...any pattern from testify. Though used rarely, it does provide a convenient way of adding more context to test failures in complex scenarios like in e2e tests.

Might try something like having each test function return an object (or func) and use that to optionally add more context, rather than the varargs.

idea: more string helpers

Unlike testify we don't have convenience of functions like Contains for strings; we should add some

idea: helper method ErrorContains

One pattern that comes up sometimes is wanting to check an error message contains some substring (particularly when the error content is generated elsewhere)

require.Contains(t, err.Error(), tc.expectedError)

turns into

must.StrContains(t, expSub, err.Error()

but this could be something like

must.ErrorContains(t, expSub, err) 

output: use better filename than interface.go

interface.go <lineno> appears in the output of failing tests. The number of times I confused myself trying to find this file in the current project is pretty high. Instead, name it something to make it obviously part of the test package.

docs: add runnable Go doc examples for all assertion functions

Go docs have a nice feature where you can provide runnable examples that show up in the generated documentation for a package. https://go.dev/blog/examples

In https://github.com/shoenig/test/blob/main/examples_test.go we now have a place to add such examples, but I only added a handful so far. It would be nice to finish adding basic examples for all of the listed functions.

Also, each example is runnable as a test, e.g.

go test -v -run Example
=== RUN   ExampleAscending
--- PASS: ExampleAscending (0.00s)
=== RUN   ExampleBetween
--- PASS: ExampleBetween (0.00s)
...

as long as it contains an // Output: comment.

PRs welcome, even if they only add an example or two!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.