eventually

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2024 License: MIT Imports: 2 Imported by: 0

README

Eventually

PkgGoDev

Eventually provides support for running a test block that should eventually succeed, while still providing access to a testing.TB throughout the whole block. Eventually will keep trying re-running the test block until either a timeout or max attempts are reached. While this was created to test asynchronous systems I suppose it might help with flaky tests too :D. Here's an example:

func TestAsync(t *testing.T) {
  asyncProcessSucceeded := false

  go func() {
    time.Sleep(100 * time.Millisecond)
    asyncProcessSucceeded = true
  }()

  eventually.Should(t, func(t testing.TB) {
    if !asyncProcessSucceeded {
      t.Fail()
    }
  })
}

Notice how within the function you pass to eventually.Should you have access to a t testing.TB, this allows you to still use your helpers and assertions that rely on the good old t. Here's another example, using testify's assert and require:

// code that sets up async consequences, e.g. writes some events on a queue

eventually.Should(t, func(t testing.TB) {
  events, err := readFromQueue()
  require.NoError(t, err)
  require.Len(t, events, 1)
  assert.Equal(t, "event", events[0])
})

Eventually has Should and Must functions, that correspond to Fail and FailNow respectively in case of failure.

Behaviour can be customised with use of Options, for example:

eventually.Should(t, func(t testing.TB) {
  // your test code here
},
  eventually.WithTimeout(10*time.Second),
  eventually.WithInterval(100*time.Millisecond),
  eventually.WithMaxAttempts(10),
)

And if you want to reuse your configuration you can do so by creating your very own Eventually. The example above would look something like:

eventually := eventually.New(
  eventually.WithTimeout(10*time.Second),
  eventually.WithInterval(100*time.Millisecond),
  eventually.WithMaxAttempts(10),
)

eventually.Should(t, func(t testing.TB) {
  // test code
})

eventually.Must(t, func(t testing.TB) {
  // test code
})

Why does this exist?

TL;DR: I like t a lot

Other testing libraries have solutions for this. Testify for instance has its own Eventually, but the function it takes returns a bool and has no access to an "inner" *testing.T to be used for helpers and assertions. Let's say for example that you have a helper function that reads a file and returns its content as a string, failing the test if it can't find the file (more convenient than handling all errors in the test itself). If the file you want to test is being created asynchronously using that helper within Eventually will halt the whole test instead of trying executing again. In Go code:

func TestAsyncFile(t *testing.T) {
  // setup

  assert.Eventually(t, func() bool {
    contents := readFile(t, "path") // <-- this halts the whole TestAsyncFile, not just this Eventually run
    return contents == "expected"
  })
}

func readFile(t *testing.T, path string) string {
  f, err := os.Open(path)
  require.NoError(t, err)

  // reading the file
}

With the addition of assert.EventuallyWithT it would seem that our problem is solved, but the issue with that is that the *CollectT available to the function panics on FailNow (which is what t.FailNow does). This means that if you have anything using FailNow (including using testify's own require) this will panic and halt the whole test, not just the EventuallyWithT block:

func TestAsyncFile(t *testing.T) {
  // setup

  assert.EventuallyWithT(t, func(t *assert.Assertions) {
    f, err := os.Open(path)
    require.NoError(t, err) // <-- this would panic and halt the whole test

    // your test code here
  })
}

Another available alternative is Gomega's [`Eventually`](https://pkg.go.dev/github.com/onsi/gomega#Eventually) (yes, this package has a very original name), which can be very convenient to use but requires buying into Gomega as a whole, which is quite the commitment (and I don't find a particularly idiomatic way of writing tests in Go but hey, opinions). This also still doesn't give access to a `t` with its own scope, you can do assertions within the `Eventually` block but if you have code that relies on `*testing.T` being around you cannot use it:

```go
gomega.Eventually(func(g gomega.Gomega) {
  contents := readFile(t, "path") // no t :(
  g.Expect(contents).To(gomega.Equal("expected"))
}).Should(gomega.Succeed())

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Must

func Must(t testing.TB, f func(t testing.TB), options ...Option)

Must will keep retrying the given function f until the testing.TB passed to it does not fail or one of the following conditions is met:

  • the timeout is reached
  • the maximum number of attempts is reached

If f does not succed, Must will halt the test calling t.Fatalf. Must behaviour can be changed by passing options to it.

func Should

func Should(t testing.TB, f func(t testing.TB), options ...Option)

Should will keep retrying the given function f until the testing.TB passed to it does not fail or one of the following conditions is met:

  • the timeout is reached
  • the maximum number of attempts is reached

If f does not succed, Should will fail the test calling t.Errorf. Should behaviour can be changed by passing options to it.

Types

type Eventually

type Eventually struct {
	// contains filtered or unexported fields
}

func New

func New(options ...Option) *Eventually

New creates a new Eventually with the given options. This can be useful if you want to reuse the same configuration for multiple functions. For example:

e := eventually.New(eventually.WithMaxAttempts(10))

The returned Eventually has the following defaults unless otherwise specified:

Timeout:     10 seconds
Interval:    100 milliseconds
MaxAttempts: 0 (unlimited)

If you don't need to reuse the same configuration, you can use the Must and Should functions directly.

func (*Eventually) Must

func (e *Eventually) Must(t testing.TB, f func(t testing.TB))

Must will keep retrying the given function f until the testing.TB passed to it does not fail or one of the following conditions is met:

  • the timeout is reached
  • the maximum number of attempts is reached

If f does not succed, Must will halt the test calling t.Fatalf.

func (*Eventually) Should

func (e *Eventually) Should(t testing.TB, f func(t testing.TB))

Should will keep retrying the given function f until the testing.TB passed to it does not fail or one of the following conditions is met:

  • the timeout is reached
  • the maximum number of attempts is reached

If f does not succed, Should will fail the test calling t.Errorf.

type Option

type Option func(*Eventually)

Option is a function that can be used to configure an Eventually.

func WithInterval

func WithInterval(interval time.Duration) Option

WithInterval sets the interval Eventually will wait between attempts.

func WithMaxAttempts

func WithMaxAttempts(attempts int) Option

WithMaxAttempts sets the maximum number of attempts an Eventually will make.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the timeout for an Eventually.

Jump to

Keyboard shortcuts

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