2019-03-23-gotest-tools-poll.org (5193B)
1 #+TITLE: Golang testing — gotest.tools poll 2 #+date: <2019-03-23 Sat> 3 #+filetags: go testing poll 4 5 #+TOC: headlines 2 6 7 * Introduction 8 Let's continue the [[https://gotest.tools][=gotest.tools=]] serie, this time with the =poll= package. 9 10 #+BEGIN_QUOTE 11 Package poll provides tools for testing asynchronous code. 12 #+END_QUOTE 13 14 When you write test, you may test a piece of code that work asynchronously, where the 15 state you're expecting is gonna take a bit of time to be achieved. This is especially true 16 when you work on networking or file-system code. And this happens a lot when you write 17 integration (or end-to-end) test, less for unit-tests. 18 19 The package =poll= is trying to tackle those use cases. We'll first take a look at the 20 main function, =WaitOn=, then how to write a ~Check~, using the ~Result~ type. 21 22 * ~WaitOn~ 23 24 Let's look into the main ~poll~ function : `WaitOn`. 25 26 #+begin_quote 27 WaitOn a condition or until a timeout. Poll by calling check and exit when check returns 28 a done Result. To fail a test and exit polling with an error return a error result. 29 #+end_quote 30 31 In a gist, ~WaitOn~ will run a /condition/ function until it either times out or 32 succeed. It wait for a given time/delay between each run. 33 34 #+begin_src go 35 func WaitOn(t TestingT, check Check, pollOps ...SettingOp) { 36 // […] 37 } 38 #+end_src 39 40 As any /testing helper/ function, the first argument is ~*testing.T~ (or, in this case, 41 any thing that look like it, thanks to the ~TestingT~ interace). The two other arguments 42 are way more interesting : 43 44 - The ~Check~ is the condition that will run multiple times until it either timeout, or succeed. 45 - The ~SettingOp(s)~ which are options to configure the function, things like the timeout, 46 or the /delay/ between each run. 47 48 The settings are pretty straightforward : 49 50 - ~WithDelay~ : sets the delay to wait between polls. The default delay is 100ms. 51 - ~WithTimeout~ : sets the timeout. The default timeout is 10s. 52 53 There is existing ~Check~ for common case: 54 55 - ~Connection~ : try to open a connection to the address on the named network. 56 57 #+begin_src go 58 poll.WaitOn(t, poll.Connection("tcp", "foo.bar:55555"), poll.WithTimeout("5s")) 59 #+end_src 60 61 - ~FileExists~ : looks on filesystem and check that path exists. 62 63 #+begin_src go 64 poll.WaitOn(t, poll.FileExists("/should/be/created"), poll.WithDelay("1s")) 65 #+end_src 66 67 68 * ~Check~ and ~Result~ 69 70 ~Connection~ and ~FileExists~ are the only two /built-in/ ~Check~ provided by 71 ~gotest.tools~. They are useful, but as usual, where ~gotest.tools~ shines is 72 extensiblity. It is really easy to define your own ~Check~. 73 74 #+begin_src go 75 type Check func(t LogT) Result 76 #+end_src 77 78 A ~Check~ is, thus, only a function that takes ~LogT~ — which is anything that can log 79 something, like ~*testing.T~ — and return a ~Result~. Let's look at this intersting 80 ~Result~ type. 81 82 #+begin_src go 83 type Result interface { 84 // Error indicates that the check failed and polling should stop, and the 85 // the has failed 86 Error() error 87 // Done indicates that polling should stop, and the test should proceed 88 Done() bool 89 // Message provides the most recent state when polling has not completed 90 Message() string 91 } 92 #+end_src 93 94 Although it's an interface, the ~poll~ package defines built-in ~Result~ so that it's easy 95 to write ~Check~ without having to define you ~Result~ type. 96 97 - ~Continue~ returns a Result that indicates to WaitOn that it should continue 98 polling. The message text will be used as the failure message if the timeout is reached. 99 - ~Success~ returns a Result where Done() returns true, which indicates to WaitOn that it 100 should stop polling and exit without an error. 101 - ~Error~ returns a Result that indicates to WaitOn that it should fail the test and stop 102 polling. 103 104 The basic just to write a ~Check~ is then : 105 106 - if the state is not there yet, return ~Continue~, 107 - if there is an error, unrelated to validating the state, return an ~Error~, 108 - if the state is there, return ~Success~. 109 110 Let's look at an example taken from the ~moby/moby~ source code. 111 112 #+begin_src go 113 poll.WaitOn(t, container.IsInState(ctx, client, cID, "running"), poll.WithDelay(100*time.Millisecond)) 114 115 func IsInState(ctx context.Context, client client.APIClient, containerID string, state ...string) func(log poll.LogT) poll.Result { 116 return func(log poll.LogT) poll.Result { 117 inspect, err := client.ContainerInspect(ctx, containerID) 118 if err != nil { 119 return poll.Error(err) 120 } 121 for _, v := range state { 122 if inspect.State.Status == v { 123 return poll.Success() 124 } 125 } 126 return poll.Continue("waiting for container to be one of (%s), currently %s", strings.Join(state, ", "), inspect.State.Status) 127 } 128 } 129 #+end_src 130 131 132 * Conclusion 133 134 … that's a wrap. The =poll= package allows to easily wait for a condition to happen in a 135 given time-frame — with sane defaults. As for most of the ~gotest.tools~ package, we use 136 this package heavily in =docker/*= projects too…