go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/testing/assert/assert.go (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package assert implements an extensible, simple, assertion library for Go 16 // with minimal dependencies. 17 // 18 // # Why have an assertion library at all? 19 // 20 // While it is recommended to use 'stdlib style' for Go tests, we have found 21 // them to be lacking in the following ways: 22 // 23 // 1. Writing good error messages is difficult; when an assertion fails, 24 // the error message needs to indicate why. As the error messages get more 25 // complex, you may be tempted to write helpers... at which point you've 26 // built an assertion library. 27 // 2. If you refuse to put the helpers in a library, you now have one custom 28 // library per package, which is worse. 29 // 3. If you ignore the problem, you may be tempted to write shorter error 30 // messages, at which point test failures become more cumbersome to debug. 31 // 32 // So, for our applications, we've found them to be helpful tools to write high 33 // quality tests. This library is NOT a requirement, but a tool. If it gets in 34 // the way, don't use it (or, perhaps better, improve it). 35 // 36 // # Why not X? 37 // 38 // At the time of writing, Go generics were relatively new, and no assertion 39 // libraries had adopted them in a meaningful way to make compilers do the 40 // legwork to make sure all the types lined up correctly when possible. 41 // 42 // One of the really bad things about early assertion libraries was that they 43 // were almost always forced to use `interface{}` (a.k.a. `any`) for inputs, and 44 // extensive amounts of "reflect" based code. 45 // 46 // This made the APIs of such assertion libraries more difficult to grok for 47 // readers, and meant that a large class of assertion failures only showed up at 48 // runtime, which was unfortunate. 49 // 50 // While this library does still do some runtime type reflection (to convert 51 // from `actual any` to `T` for the given Comparison), this conversion is done 52 // in exactly one place (this package), and does not require each comparison to 53 // do this. 54 // 55 // # Why now, and why this style? 56 // 57 // At the time this library was written, our codebase had a large amount of testing 58 // code written with `github.com/smartystreets/goconvey/convey` which is a "BDD 59 // style" testing framework (sort of). We liked the assertion syntax well enough 60 // to emulate it here; in that framework assertions look like: 61 // 62 // So(actualValue, ShouldResemble, expectedValue) 63 // So(somePointer, ShouldBeNil) 64 // So(aString, ShouldBeOneOf, "a", "b", "c") 65 // 66 // However, this framework had the problem that assertion functions are 67 // difficult to document (since their signature is always 68 // `func(any, ...any) string`), had an extra implementation burden for 69 // implementing custom checkers (every implementation, even small helpers inside 70 // of a test package, had to do type-casting on the expected arguments, ensure 71 // that the right number of expected values, etc.). 72 // 73 // Further, the return type of `string` is also dissatisfyingly untyped... there 74 // were global variables which could manipulate the assertions so that they 75 // returned encoded JSON instead of a plain string message. 76 // 77 // For goconvey assertions, you also had to also use the controversial "Convey" 78 // suite syntax (see the sister library `ftt` adjacent to `assert`, which 79 // implements the test layout/format without the "BDD style" flavoring). 80 // This `assert` library has no such restriction. 81 // 82 // This library is a way for us to provide high quality assertion replacements 83 // for the So assertions of goconvey. 84 // 85 // # Usage 86 // 87 // import ( 88 // "testing" 89 // 90 // // Exports EXACTLY two symbols, Assert and Check. 91 // . "go.chromium.org/luci/common/testing/assert" 92 // 93 // // Optional; these are a collection of useful common comparisons, but 94 // // are by no means required. 95 // "go.chromium.org/luci/common/testing/assert/should" 96 // ) 97 // 98 // func TestSomething(t *testing.T) { 99 // // Checks that `someFunction` returns some value assignable to `int`. 100 // // which equals 100. 101 // Assert(t, someFunction(), should.Equal(100)) 102 // 103 // // Checks that `someFunction` returns some value assignable to `int8` 104 // // which equals 100. 105 // Assert(t, someFunction(), should.Equal[int8](100)) 106 // 107 // // Checks that `someFunction` returns some value assignable to 108 // // `*someStruct` which is populated in the same way. 109 // // 110 // // NOTE: should.Resemble correctly handles comparisons between protobufs 111 // // and types containing protobufs, by default. 112 // Assert(t, someFunctionReturningStruct(), should.Resemble(&someStruct{ 113 // ... 114 // })) 115 // } 116 package assert 117 118 import ( 119 "reflect" 120 121 "go.chromium.org/luci/common/data" 122 "go.chromium.org/luci/common/testing/assert/interfaces" 123 "go.chromium.org/luci/common/testing/assert/results" 124 ) 125 126 // Assert compares `actual` using `compare`, which is typically a closure over some 127 // expected value. 128 // 129 // If `comparison` returns a non-nil Result, this logs it and calls t.FailNow(). 130 // 131 // `actual` will be converted to T using the function 132 // [go.chromium.org/luci/common/data.LosslessConvertTo]. 133 // 134 // If this conversion fails, a descriptive error will be logged and FailNow() 135 // called. 136 // 137 // `testingTB` is an interface which is a subset of testing.TB, but is 138 // unexported to allow this package to be cleanly .-imported. 139 func Assert[T any](t interfaces.TestingTB, actual any, compare results.Comparison[T]) { 140 t.Helper() 141 142 if !Check[T](t, actual, compare) { 143 t.FailNow() 144 } 145 } 146 147 // Check compares `actual` using `compare`, which is typically a closure over some 148 // expected value. 149 // 150 // If `comparison` returns a non-nil Result, this logs it and calls t.Fail(), 151 // returning true iff the comparison was successful. 152 // 153 // `actual` will be converted to T using the function 154 // [go.chromium.org/luci/common/data.LosslessConvertTo]. 155 // 156 // `testingTB` is an interface which is a subset of testing.TB, but is 157 // unexported to allow this package to be cleanly .-imported. 158 func Check[T any](t interfaces.TestingTB, actual any, compare results.Comparison[T]) bool { 159 t.Helper() 160 161 actualTyped, ok := data.LosslessConvertTo[T](actual) 162 var result *results.Result 163 if !ok { 164 result = results.NewResultBuilder(). 165 SetName("builtin.LosslessConvertTo", reflect.TypeOf(&actualTyped)). 166 Result() 167 } else { 168 result = compare(actualTyped) 169 } 170 171 if result != nil { 172 for _, line := range result.Render() { 173 t.Log(line) 174 } 175 t.Fail() 176 return false 177 } 178 return true 179 }