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
Checkis 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.
Continuereturns 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.Successreturns a Result where Done() returns true, which indicates to WaitOn that it should stop polling and exit without an error.Errorreturns 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…