2018-08-16-gotest-tools-assertions.org (15197B)
1 #+title: Golang testing — gotest.tools assertions 2 #+date: <2018-08-16 Thu> 3 #+filetags: go testing assert 4 #+setupfile: ../templates/post.org 5 6 #+TOC: headlines 2 7 8 * Introduction 9 10 Let's take a closer look at [[https://gotest.tools][=gotest.tools=]] assertions packages. This is mainly about =assert=, =assert/cmp= and 11 =assert/opt=. 12 13 #+BEGIN_QUOTE 14 Package assert provides assertions for comparing expected values to actual values. When assertion fails a helpful error 15 message is printed. 16 #+END_QUOTE 17 18 There is two main functions (=Assert= and =Check=) and some helpers (like =NilError=, …). They all take a =*testing.T= as 19 a first argument, pretty common across testing Go libraries. Let's dive into those ! 20 21 * =Assert= and =Check= 22 23 Both those functions accept a =Comparison= (we'll check what it is later on) and fail the test when that comparison 24 fails. The one difference is that =Assert= will end the test execution at immediately whereas =Check= will fail the test 25 and proceed with the rest of the test case. This is similar to =FailNow= and =Fail= from the standard library 26 =testing=. Both have their use cases. 27 28 We'll Use =Assert= for the rest of the section but any example here would work with =Check= too. When we said 29 =Comparison= above, it's mainly the [[https://godoc.org/gotest.tools/assert#BoolOrComparison][BoolOrComparison]] interface — it can either be a boolean expression, or a 30 [[https://godoc.org/gotest.tools/assert/cmp#Comparison][cmp.Comparison]] type. =Assert= and =Check= code will be /smart/ enough to detect which one it is. 31 32 #+BEGIN_SRC go 33 assert.Assert(t, ok) 34 assert.Assert(t, err != nil) 35 assert.Assert(t, foo.IsBar()) 36 #+END_SRC 37 38 So far not anything extra-ordinary. Let's first look at some more /helper/ functions in the =assert= package and quickly 39 dive a bit deeper with =Comparison=. 40 41 * More =assert= helpers 42 43 The additional helper functions are the following 44 45 - =Equal= uses the ==== operator to assert two values are equal. 46 - =DeepEqual= uses =google/go-cmp= to assert two values are equal (it's /close/ to =reflect.DeepEqual= but not 47 quite). We'll detail a bit more the /options/ part of this function with =cmp.DeepEqual=. 48 - =Error= fails if the error is =nil= *or* the error message is not the expected one. 49 - =ErrorContains= fails if the error is =nil= *or* the error message does not contain the expected substring. 50 - =ErrorType= fails if the error is =nil= *or* the error type is not the expected type. 51 - =NilError= fails if the error is not =nil=. 52 53 All those helper functions have a equivalent function in the =cmp= package that returns a =Comparison=. I, personally, 54 prefer to use =assert.Check= or =assert.Assert= in combination with =cmp.Comparison= as it allows me to write all my 55 assertions the same way, with built-ins comparison or with my own — i.e. =assert.Assert(t, is.Equal(…), "message"= or 56 =assert.Assert(t, stackIsUp(c, time…), "another message")=. 57 58 * =cmp.Comparison= 59 60 This is where it get really interesting, =gotest.tools= tries to make it as easy as possible for you to create 61 appropriate comparison — making you test readable as much as possible. 62 63 Let's look a bit at the =cmp.Comparison= type. 64 65 #+BEGIN_SRC go 66 type Comparison func() Result 67 #+END_SRC 68 69 It's just a function that returns a =cmp.Result=, so let's look at =cmp.Result= definition. 70 71 #+BEGIN_SRC go 72 type Result interface { 73 Success() bool 74 } 75 #+END_SRC 76 77 Result is an =interface=, thus any /struct/ that provide a function =Success= that returns a =bool= can be used as a 78 comparison result, making it really easy to use in your code. There is also existing type of result to make it even 79 quicker to write your own comparison. 80 81 - =ResultSuccess= is a constant which is returned to indicate success. 82 - =ResultFailure= and =ResultFailureTemplate= return a failed Result with a failure message. 83 - =ResultFromError= returns =ResultSuccess= if =err= is nil. Otherwise =ResultFailure= is returned with the error 84 message as the failure message. It works a bit like the =errors.Wrap= function of the [[https://github.com/pkg/errors][=github.com/pkgs/errors=]] 85 package. 86 87 The =cmp= package comes with a few defined comparison that, we think, should cover a high number of use-cases. Let's 88 look at them. 89 90 ** Equality with =Equal= and =DeepEqual= 91 92 #+BEGIN_QUOTE 93 Equal uses the == operator to assert two values are equal and fails the test if they are not equal. 94 95 If the comparison fails Equal will use the variable names for x and y as part of the failure message to identify the 96 actual and expected values. 97 98 If either x or y are a multi-line string the failure message will include a unified diff of the two values. If the 99 values only differ by whitespace the unified diff will be augmented by replacing whitespace characters with visible 100 characters to identify the whitespace difference. 101 #+END_QUOTE 102 103 On the other hand… 104 105 #+BEGIN_QUOTE 106 DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are equal and fails the test if they are not 107 equal. 108 109 Package https://godoc.org/gotest.tools/assert/opt provides some additional commonly used Options. 110 #+END_QUOTE 111 112 Using one or the other is as simple as : if you wrote your =if= with ==== then use =Equal=, otherwise use =DeepEqual=. 113 =DeepEqual= (and usually =reflect.DeepEqual=) is used when you want to compare anything more complex than primitive 114 types. One advantage of using =cmp.DeepEqual= over =reflect.DeepEqual= (in an if), is that you get a well crafted 115 message that shows the diff between the expected and the actual structs compared – and you can pass options to it. 116 117 #+BEGIN_SRC go 118 assert.Assert(t, cmp.DeepEqual([]string{"a", "b"}, []string{"b", "a"})) 119 // Will print something like 120 // --- result 121 // +++ exp 122 // {[]string}[0]: 123 // -: "a" 124 // +: "b" 125 // {[]string}[1]: 126 // -: "b" 127 // +: "a" 128 foo := &someType(a: "with", b: "value") 129 bar := &someType(a: "with", b: "value") 130 // the following will succeed as foo and bar are _DeepEqual_ 131 assert.Assert(t, cmp.DeepEqual(foo, bar)) 132 #+END_SRC 133 134 When using =DeepEqual=, you may end up with really weird behavior(s). You may want to ignore some fields, or consider 135 =nil= slice or map the same as empty ones ; or more common, your /struct/ contains some unexported fields that you 136 cannot use when comparing (as they are not exported 😓). In those case, you can use =go-cmp= options. 137 138 Some existing one are : 139 - [[https://godoc.org/github.com/google/go-cmp/cmp/cmpopts#EquateEmpty][=EquateEmpty=]] returns a Comparer option that determines all maps and slices with a length of zero to be equal, 140 regardless of whether they are nil. 141 - [[https://godoc.org/github.com/google/go-cmp/cmp/cmpopts#IgnoreFields][=IgnoreFields=]] returns an Option that ignores exported fields of the given names on a single struct type. The struct 142 type is specified by passing in a value of that type. 143 - [[https://godoc.org/github.com/google/go-cmp/cmp/cmpopts#IgnoreUnexported][=IgnoreUnexported=]] returns an Option that only ignores the immediate unexported fields of a struct, including anonymous 144 fields of unexported types. 145 - [[https://godoc.org/github.com/google/go-cmp/cmp/cmpopts#SortSlices][=SortSlices=]] returns a Transformer option that sorts all =[]V= 146 - … and [[https://godoc.org/github.com/google/go-cmp/cmp/cmpopts][more]] 👼 147 148 =gotest.tools= also defines some *and* you can define yours ! As an example, =gotest.tools= defines =TimeWithThreshold= 149 and =DurationWithThreshold= that allows to not fails if the time (or duration) is not exactly the same but in the 150 specified threshold we specified. Here is the code for =DurationWithThreshold= for inspiration. 151 152 #+BEGIN_SRC go 153 // DurationWithThreshold returns a gocmp.Comparer for comparing time.Duration. The 154 // Comparer returns true if the difference between the two Duration values is 155 // within the threshold and neither value is zero. 156 func DurationWithThreshold(threshold time.Duration) gocmp.Option { 157 return gocmp.Comparer(cmpDuration(threshold)) 158 } 159 160 func cmpDuration(threshold time.Duration) func(x, y time.Duration) bool { 161 return func(x, y time.Duration) bool { 162 if x == 0 || y == 0 { 163 return false 164 } 165 delta := x - y 166 return delta <= threshold && delta >= -threshold 167 } 168 } 169 #+END_SRC 170 171 Another good example for those options is when you want to skip some field. In [[https://github.com/docker/docker][=docker/docker=]] we want to be able to 172 easily check for equality between two service specs, but those might have different =CreatedAt= and =UpdatedAt= values 173 that we usually don't care about – what we want is to make sure it happens in the past 20 seconds. You can easily define 174 an option for that. 175 176 #+BEGIN_SRC go 177 func cmpServiceOpts() cmp.Option { 178 const threshold = 20 * time.Second 179 180 // Apply withinThreshold only for the following fields 181 metaTimeFields := func(path cmp.Path)bool { 182 switch path.String() { 183 case "Meta.CreatedAt", "Meta.UpdatedAt": 184 return true 185 } 186 return false 187 } 188 // have a 20s threshold for the time value that will be passed 189 withinThreshold := cmp.Comparer(func(x, y time.Time) bool { 190 delta := x.Sub(y) 191 return delta < threshold && delta > -threshold 192 }) 193 194 return cmp.FilterPath(metaTimeFields, withinThreshold) 195 } 196 #+END_SRC 197 198 I recommend you look at the [[https://godoc.org/gotest.tools/assert/opt][gotest.tools/assert/opt]] documentation to see which one are defined and how to use them. 199 200 ** Errors with =Error=, =ErrorContains= and =ErrorType= 201 202 Checking for errors is *very common* in Go, having =Comparison= function for it was a requirement. 203 204 - =Error= fails if the error is =nil= *or* the error message is not the expected one. 205 - =ErrorContains= fails if the error is =nil= *or* the error message does not contain the expected substring. 206 - =ErrorType= fails if the error is =nil= *or* the error type is not the expected type. 207 208 Let's first look at the most used : =Error= and =ErrorContains=. 209 210 #+BEGIN_SRC go 211 var err error 212 // will fail with : expected an error, got nil 213 assert.Check(t, cmp.Error(err, "message in a bottle")) 214 err = errors.Wrap(errors.New("other"), "wrapped") 215 // will fail with : expected error "other", got "wrapped: other" 216 assert.Check(t, cmp.Error(err, "other")) 217 // will succeed 218 assert.Check(t, cmp.ErrorContains(err, "other")) 219 #+END_SRC 220 221 As you can see =ErrorContains= is especially useful when working with /wrapped/ errors. 222 Now let's look at =ErrorType=. 223 224 #+BEGIN_SRC go 225 var err error 226 // will fail with : error is nil, not StubError 227 assert.Check(t, cmp.ErrorType(err, StubError{})) 228 229 err := StubError{"foo"} 230 // will succeed 231 assert.Check(t, cmp.ErrorType(err, StubError{})) 232 233 // Note that it also work with a function returning an error 234 func foo() error {} 235 assert.Check(t, cmp.ErrorType(foo, StubError{})) 236 #+END_SRC 237 238 ** Bonus with =Panics= 239 240 Sometimes, a code is supposed to /panic/, see [[https://golang.org/doc/effective_go.html#panic][Effective Go (#Panic)]] for more information. And thus, you may want to make 241 sure you're code panics in such cases. It's always a bit tricky to test a code that panic as you have to use a deferred 242 function to recover the panic — but then if the panic doesn't happen how do you fail the test ? 243 244 This is where =Panics= comes handy. 245 246 #+BEGIN_SRC go 247 func foo(shouldPanic bool) { 248 if shouldPanic { 249 panic("booooooooooh") 250 } 251 // don't worry, be happy 252 } 253 // will fail with : did not panic 254 assert.Check(t, cmp.Panics(foo(false))) 255 // will succeed 256 assert.Check(t, cmp.Panics(foo(true))) 257 #+END_SRC 258 259 ** Miscellaneous with =Contains=, =Len= and =Nil= 260 261 Those last three /built-in/ =Comparison= are pretty straightforward. 262 263 - =Contains= succeeds if item is in collection. Collection may be a string, map, slice, or array. 264 265 If collection is a string, item must also be a string, and is compared using =strings.Contains()=. If collection is a 266 Map, contains will succeed if item is a key in the map. If collection is a slice or array, item is compared to each 267 item in the sequence using ==reflect.DeepEqual()==. 268 - =Len= succeeds if the sequence has the expected length. 269 - =Nil= succeeds if obj is a nil interface, pointer, or function. 270 271 #+BEGIN_SRC go 272 // Contains works on string, map, slice or arrays 273 assert.Check(t, cmp.Contains("foobar", "foo")) 274 assert.Check(t, cmp.Contains([]string{"a", "b", "c"}, "b")) 275 assert.Check(t, cmp.Contains(map[string]int{"a": 1, "b": 2, "c": 4}, "b")) 276 277 // Len also works on string, map, slice or arrays 278 assert.Check(t, cmp.Len("foobar", 6)) 279 assert.Check(t, cmp.Len([]string{"a", "b", "c"}, 3)) 280 assert.Check(t, cmp.Len(map[string]int{"a": 1, "b": 2, "c": 4}, 3)) 281 282 // Nil 283 var foo *MyStruc 284 assert.Check(t, cmp.Nil(foo)) 285 assert.Check(t, cmp.Nil(bar())) 286 #+END_SRC 287 288 But let's not waste more time and let's see how to write our own =Comparison= ! 289 290 ** Write your own =Comparison= 291 292 One of the main aspect of =gotest.tools/assert= is to make it easy for developer to write as less boilerplate code as 293 possible while writing tests. Writing your own =Comparison= allows you to write a well named function that will be easy 294 to read and that can be re-used across your tests. 295 296 Let's look back at the =cmp.Comparison= and =cmp.Result= types. 297 298 #+BEGIN_SRC go 299 type Comparison func() Result 300 301 type Result interface { 302 Success() bool 303 } 304 #+END_SRC 305 306 A =Comparison= for =assert.Check= or =assert.Check= is a function that return a =Result=, it's pretty straightforward to 307 implement, especially with =cmp.ResultSuccess= and =cmp.ResultFailure(…)= (as seen previously). 308 309 #+BEGIN_SRC go 310 func regexPattern(value string, pattern string) cmp.Comparison { 311 return func() cmp.Result { 312 re := regexp.MustCompile(pattern) 313 if re.MatchString(value) { 314 return cmp.ResultSuccess 315 } 316 return cmp.ResultFailure( 317 fmt.Sprintf("%q did not match pattern %q", value, pattern)) 318 } 319 } 320 321 // To use it 322 assert.Check(t, regexPattern("12345.34", `\d+.\d\d`)) 323 #+END_SRC 324 325 As you can see, it's pretty easy to implement, and you can do quite a lot in there easily. If a function call returns an 326 error inside of your =Comparison= function, you can use =cmp.ResultFromError= for example. Having something like 327 =assert.Check(t, isMyServerUp(":8080"))= is way more readable than a 30-line of code to check it. 328 329 * Conclusion… 330 331 … and that's a wrap. We only looked at the =assert= package of [[https://gotest.tools][=gotest.tools=]] so far, but it's already quite a bit to process. 332 333 We've seen : 334 - the main functions provided by this package : =assert.Assert= and =assert.Check= 335 - some helper functions like =assert.NilError=, … 336 - the =assert/cmp=, and =assert/opt= sub-package that allows you to write more custom =Comparison= 337 338 Next time, we'll look at the =skip= package, that is a really simple wrapper on top of =testing.Skip= function. 339 340 **