home

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 **