Golang testing — gotest.tools poll

Introduction

Let’s continue the gotest.tools serie, this time with the poll package.

Package poll provides tools for testing asynchronous code.

When you write test, you may test a piece of code that work asynchronously, where the state you’re expecting is gonna take a bit of time to be achieved. This is especially true when you work on networking or file-system code. And this happens a lot when you write integration (or end-to-end) test, less for unit-tests.

The package poll is trying to tackle those use cases. We’ll first take a look at the main function, WaitOn, then how to write a Check, using the Result type.

WaitOn

Let’s look into the main poll function : `WaitOn`.

WaitOn a condition or until a timeout. Poll by calling check and exit when check returns a done Result. To fail a test and exit polling with an error return a error result.

In a gist, WaitOn will run a condition function until it either times out or succeed. It wait for a given time/delay between each run.

func WaitOn(t TestingT, check Check, pollOps ...SettingOp) {
        // […]
}

As any testing helper function, the first argument is *testing.T (or, in this case, any thing that look like it, thanks to the TestingT interace). The two other arguments are way more interesting :

  • The Check is the condition that will run multiple times until it either timeout, or succeed.
  • The SettingOp(s) which are options to configure the function, things like the timeout, or the delay between each run.

The settings are pretty straightforward :

  • WithDelay : sets the delay to wait between polls. The default delay is 100ms.
  • WithTimeout : sets the timeout. The default timeout is 10s.

There is existing Check for common case:

  • Connection : try to open a connection to the address on the named network.

    poll.WaitOn(t, poll.Connection("tcp", "foo.bar:55555"), poll.WithTimeout("5s"))
    
  • FileExists : looks on filesystem and check that path exists.

    poll.WaitOn(t, poll.FileExists("/should/be/created"), poll.WithDelay("1s"))
    

Check and Result

Connection and FileExists are the only two built-in Check provided by gotest.tools. They are useful, but as usual, where gotest.tools shines is extensiblity. It is really easy to define your own Check.

type Check func(t LogT) Result

A Check is, thus, only a function that takes LogT — which is anything that can log something, like *testing.T — and return a Result. Let’s look at this intersting Result type.

type Result interface {
    // Error indicates that the check failed and polling should stop, and the
    // the has failed
    Error() error
    // Done indicates that polling should stop, and the test should proceed
    Done() bool
    // Message provides the most recent state when polling has not completed
    Message() string
}

Although it’s an interface, the poll package defines built-in Result so that it’s easy to write Check without having to define you Result type.

  • Continue returns a Result that indicates to WaitOn that it should continue polling. The message text will be used as the failure message if the timeout is reached.
  • Success returns a Result where Done() returns true, which indicates to WaitOn that it should stop polling and exit without an error.
  • Error returns a Result that indicates to WaitOn that it should fail the test and stop polling.

The basic just to write a Check is then :

  • if the state is not there yet, return Continue,
  • if there is an error, unrelated to validating the state, return an Error,
  • if the state is there, return Success.

Let’s look at an example taken from the moby/moby source code.

poll.WaitOn(t, container.IsInState(ctx, client, cID, "running"), poll.WithDelay(100*time.Millisecond))

func IsInState(ctx context.Context, client client.APIClient, containerID string, state ...string) func(log poll.LogT) poll.Result {
        return func(log poll.LogT) poll.Result {
                inspect, err := client.ContainerInspect(ctx, containerID)
                if err != nil {
                        return poll.Error(err)
                }
                for _, v := range state {
                        if inspect.State.Status == v {
                                return poll.Success()
                        }
                }
                return poll.Continue("waiting for container to be one of (%s), currently %s", strings.Join(state, ", "), inspect.State.Status)
        }
}

Conclusion

… that’s a wrap. The poll package allows to easily wait for a condition to happen in a given time-frame — with sane defaults. As for most of the gotest.tools package, we use this package heavily in docker/* projects too…