Golang testing — gotest.tools poll
Sat, 23 March, 2019 (700 Words)
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…