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  }