├── go.mod ├── Makefile ├── .github └── workflows │ ├── release.yml │ └── build.yml ├── LICENSE ├── README.md ├── be.go └── be_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nalgeon/be 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: vet lint test 2 | 3 | lint: 4 | @golangci-lint run ./... 5 | @echo "✓ lint" 6 | 7 | vet: 8 | @go vet ./... 9 | @echo "✓ vet" 10 | 11 | test: 12 | @go test ./... 13 | @echo "✓ test" 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Create a draft release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Create release draft 20 | env: 21 | GH_TOKEN: ${{ github.token }} 22 | run: gh release create ${{ github.ref_name }} --generate-notes --draft 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - README.md 8 | pull_request: 9 | branches: [main] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Build and test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: "go.mod" 24 | 25 | - name: Install linter 26 | uses: golangci/golangci-lint-action@v8 27 | 28 | - name: Build and test 29 | run: make all 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anton Zhiyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Be - a minimal test assertions package 2 | 3 | If you want simple test assertions and feel like [testify](https://pkg.go.dev/github.com/attic-labs/testify/assert) is too much, but [is](https://pkg.go.dev/github.com/matryer/is) is too basic, you might like `be`. 4 | 5 | Highlights: 6 | 7 | - Minimal API: `Equal`, `Err`, and `True` assertions. 8 | - Correctly compares `time.Time` values and other types with an `Equal` method. 9 | - Flexible error assertions: check if an error exists, check its value, type, or any combination of these. 10 | - Zero hassle. 11 | 12 | Be is new, but it's ready for production (or maybe I should say "testing" :) I've used it in three very different projects — a CLI tool, an API server, and a database engine — and it worked great every time. 13 | 14 | ## Usage 15 | 16 | Install with go get: 17 | 18 | ```text 19 | go get github.com/nalgeon/be 20 | ``` 21 | 22 | `Equal` asserts that two values are equal: 23 | 24 | ```go 25 | func Test(t *testing.T) { 26 | t.Run("pass", func(t *testing.T) { 27 | got, want := "hello", "hello" 28 | be.Equal(t, got, want) 29 | // ok 30 | }) 31 | 32 | t.Run("fail", func(t *testing.T) { 33 | got, want := "olleh", "hello" 34 | be.Equal(t, got, want) 35 | // got: "olleh"; want: "hello" 36 | }) 37 | } 38 | ``` 39 | 40 | Or that a value matches any of the given values: 41 | 42 | ```go 43 | func Test(t *testing.T) { 44 | got := 2 * 3 * 7 45 | be.Equal(t, got, 21, 42, 84) 46 | // ok 47 | } 48 | ``` 49 | 50 | `Err` asserts that there is an error: 51 | 52 | ```go 53 | func Test(t *testing.T) { 54 | _, err := regexp.Compile("he(?o") // invalid 55 | be.Err(t, err) 56 | // ok 57 | } 58 | ``` 59 | 60 | Or that there are no errors: 61 | 62 | ```go 63 | func Test(t *testing.T) { 64 | _, err := regexp.Compile("he??o") // valid 65 | be.Err(t, err, nil) 66 | // ok 67 | } 68 | ``` 69 | 70 | Or that an error message contains a substring: 71 | 72 | ```go 73 | func Test(t *testing.T) { 74 | _, err := regexp.Compile("he(?o") // invalid 75 | be.Err(t, err, "invalid or unsupported") 76 | // ok 77 | } 78 | ``` 79 | 80 | Or that an error matches the expected error according to `errors.Is`: 81 | 82 | ```go 83 | func Test(t *testing.T) { 84 | err := &fs.PathError{ 85 | Op: "open", 86 | Path: "file.txt", 87 | Err: fs.ErrNotExist, 88 | } 89 | be.Err(t, err, fs.ErrNotExist) 90 | // ok 91 | } 92 | ``` 93 | 94 | Or that the error type matches the expected type according to `errors.As`: 95 | 96 | ```go 97 | func Test(t *testing.T) { 98 | got := &fs.PathError{ 99 | Op: "open", 100 | Path: "file.txt", 101 | Err: fs.ErrNotExist, 102 | } 103 | be.Err(t, got, reflect.TypeFor[*fs.PathError]()) 104 | // ok 105 | } 106 | ``` 107 | 108 | Or a mix of the above: 109 | 110 | ```go 111 | func Test(t *testing.T) { 112 | err := AppError("oops") 113 | be.Err(t, err, 114 | "failed", 115 | AppError("oops"), 116 | reflect.TypeFor[AppError](), 117 | ) 118 | // ok 119 | } 120 | ``` 121 | 122 | `True` asserts that an expression is true: 123 | 124 | ```go 125 | func Test(t *testing.T) { 126 | s := "go is awesome" 127 | be.True(t, len(s) > 0) 128 | // ok 129 | } 130 | ``` 131 | 132 | That's it! 133 | 134 | ## Design decisions 135 | 136 | Be is opinionated. It only has three assert functions, which are perfectly enough to write good tests. 137 | 138 | Unlike other testing packages, Be doesn't support custom error messages. When a test fails, you'll end up checking the code anyway, so why bother? The line number shows the way. 139 | 140 | Be has flexible error assertions. You don't need to choose between `Error`, `ErrorIs`, `ErrorAs`, `ErrorContains`, `NoError`, or anything like that — just use `be.Err`. It covers everything. 141 | 142 | Be doesn't fail the test when an assertion fails, so you can see all the errors at once instead of hunting them one by one. The only exception is when the `be.Err(err, nil)` assertion fails — this means there was an unexpected error. In this case, the test terminates immediately because any following assertions probably won't make sense and could cause panics. 143 | 144 | The parameter order is (got, want), not (want, got). It just feels more natural — like saying "account balance is 100 coins" instead of "100 coins is the account balance". 145 | 146 | Be has ≈150 lines of code (+500 lines for tests). For comparison, `is` has ≈250 loc (+250 lines for tests). 147 | 148 | ## Contributing 149 | 150 | Bug fixes are welcome. For anything other than bug fixes, please open an issue first to discuss your proposed changes. The package has a very limited scope, so it's important to discuss any new features before implementing them. 151 | 152 | Make sure to add or update tests as needed. 153 | 154 | ## License 155 | 156 | Created by [Anton Zhiyanov](https://antonz.org/). Released under the MIT License. 157 | -------------------------------------------------------------------------------- /be.go: -------------------------------------------------------------------------------- 1 | // Package be provides minimal assertions for Go tests. 2 | // 3 | // It only has three functions: [Equal], [Err], and [True], 4 | // which are perfectly enough to write good tests. 5 | // 6 | // Example usage: 7 | // 8 | // func Test(t *testing.T) { 9 | // re, err := regexp.Compile("he??o") 10 | // be.Err(t, err, nil) // expects no error 11 | // be.True(t, strings.Contains(re.String(), "?")) 12 | // be.Equal(t, re.String(), "he??o") 13 | // } 14 | package be 15 | 16 | import ( 17 | "bytes" 18 | "errors" 19 | "fmt" 20 | "reflect" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | // equaler is an interface for types with an Equal method 26 | // (like time.Time or net.IP). 27 | type equaler[T any] interface { 28 | Equal(T) bool 29 | } 30 | 31 | // Equal asserts that got is equal to any of the wanted values. 32 | func Equal[T any](tb testing.TB, got T, wants ...T) { 33 | tb.Helper() 34 | 35 | if len(wants) == 0 { 36 | tb.Fatal("no wants given") 37 | return 38 | } 39 | 40 | // Check if got matches any of the wants. 41 | for _, want := range wants { 42 | if areEqual(got, want) { 43 | return 44 | } 45 | } 46 | 47 | // There are no matches, report the failure. 48 | if len(wants) == 1 { 49 | // There is only one want, report it directly. 50 | tb.Errorf("got: %#v; want: %#v", got, wants[0]) 51 | return 52 | } 53 | // There are multiple wants, report a summary. 54 | tb.Errorf("got: %#v; want any of: %v", got, wants) 55 | } 56 | 57 | // Err asserts that the got error matches any of the wanted values. 58 | // The matching logic depends on want: 59 | // - If want is nil, checks if got is nil. 60 | // - If want is a string, checks if got.Error() contains want. 61 | // - If want is an error, checks if its value is found 62 | // in the got's error tree using [errors.Is]. 63 | // - If want is a [reflect.Type], checks if its type is found 64 | // in the got's error tree using [errors.As]. 65 | // - Otherwise fails the check. 66 | // 67 | // If no wants are given, checks if got is not nil. 68 | func Err(tb testing.TB, got error, wants ...any) { 69 | tb.Helper() 70 | 71 | // If no wants are given, we expect got to be a non-nil error. 72 | if len(wants) == 0 { 73 | if got == nil { 74 | tb.Error("got: ; want: error") 75 | } 76 | return 77 | } 78 | 79 | // Special case: there's only one want, it's nil, but got is not nil. 80 | // This is a fatal error, so we fail the test immediately. 81 | if len(wants) == 1 && wants[0] == nil { 82 | if got != nil { 83 | tb.Fatalf("unexpected error: %v", got) 84 | return 85 | } 86 | } 87 | 88 | // Check if got matches any of the wants. 89 | var message string 90 | for _, want := range wants { 91 | errMsg := checkErr(got, want) 92 | if errMsg == "" { 93 | return 94 | } 95 | if message == "" { 96 | message = errMsg 97 | } 98 | } 99 | 100 | // There are no matches, report the failure. 101 | if len(wants) == 1 { 102 | // There is only one want, report it directly. 103 | tb.Error(message) 104 | return 105 | } 106 | // There are multiple wants, report a summary. 107 | tb.Errorf("got: %T(%v); want any of: %v", got, got, wants) 108 | } 109 | 110 | // True asserts that got is true. 111 | func True(tb testing.TB, got bool) { 112 | tb.Helper() 113 | if !got { 114 | tb.Error("got: false; want: true") 115 | } 116 | } 117 | 118 | // areEqual checks if a and b are equal. 119 | func areEqual[T any](a, b T) bool { 120 | // Check if both are nil. 121 | if isNil(a) && isNil(b) { 122 | return true 123 | } 124 | 125 | // Try to compare using an Equal method. 126 | if eq, ok := any(a).(equaler[T]); ok { 127 | return eq.Equal(b) 128 | } 129 | 130 | // Special case for byte slices. 131 | if aBytes, ok := any(a).([]byte); ok { 132 | bBytes := any(b).([]byte) 133 | return bytes.Equal(aBytes, bBytes) 134 | } 135 | 136 | // Fallback to reflective comparison. 137 | return reflect.DeepEqual(a, b) 138 | } 139 | 140 | // isNil checks if v is nil. 141 | func isNil(v any) bool { 142 | if v == nil { 143 | return true 144 | } 145 | 146 | // A non-nil interface can still hold a nil value, 147 | // so we must check the underlying value. 148 | rv := reflect.ValueOf(v) 149 | switch rv.Kind() { 150 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer: 151 | return rv.IsNil() 152 | default: 153 | return false 154 | } 155 | } 156 | 157 | // checkErr checks if got error matches the want value. 158 | // Returns an empty string if there's a match. 159 | // Otherwise, returns an error message. 160 | func checkErr(got error, want any) string { 161 | if want != nil && got == nil { 162 | return "got: ; want: error" 163 | } 164 | 165 | switch w := want.(type) { 166 | case nil: 167 | if got != nil { 168 | return fmt.Sprintf("unexpected error: %v", got) 169 | } 170 | case string: 171 | if !strings.Contains(got.Error(), w) { 172 | return fmt.Sprintf("got: %q; want: %q", got.Error(), w) 173 | } 174 | case error: 175 | if !errors.Is(got, w) { 176 | return fmt.Sprintf("got: %T(%v); want: %T(%v)", got, got, w, w) 177 | } 178 | case reflect.Type: 179 | target := reflect.New(w).Interface() 180 | if !errors.As(got, target) { 181 | return fmt.Sprintf("got: %T; want: %s", got, w) 182 | } 183 | default: 184 | return fmt.Sprintf("unsupported want type: %T", want) 185 | } 186 | return "" 187 | } 188 | -------------------------------------------------------------------------------- /be_test.go: -------------------------------------------------------------------------------- 1 | package be_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "math/rand/v2" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/nalgeon/be" 13 | ) 14 | 15 | // mockTB is a mock implementation of testing.TB 16 | // to capture test failures. 17 | type mockTB struct { 18 | testing.TB 19 | failed bool 20 | fatal bool 21 | msg string 22 | } 23 | 24 | func (m *mockTB) Helper() {} 25 | 26 | func (m *mockTB) Fatal(args ...any) { 27 | m.fatal = true 28 | m.Error(args...) 29 | } 30 | 31 | func (m *mockTB) Fatalf(format string, args ...any) { 32 | m.fatal = true 33 | m.Errorf(format, args...) 34 | } 35 | 36 | func (m *mockTB) Error(args ...any) { 37 | m.failed = true 38 | m.msg = fmt.Sprint(args...) 39 | } 40 | 41 | func (m *mockTB) Errorf(format string, args ...any) { 42 | m.failed = true 43 | m.msg = fmt.Sprintf(format, args...) 44 | } 45 | 46 | // intType wraps an int value. 47 | type intType struct { 48 | val int 49 | } 50 | 51 | // noisy provides an Equal method. 52 | type noisy struct { 53 | val int 54 | noise float64 55 | } 56 | 57 | func newNoisy(val int) noisy { 58 | return noisy{val: val, noise: rand.Float64()} 59 | } 60 | 61 | func (n noisy) Equal(other noisy) bool { 62 | return n.val == other.val 63 | } 64 | 65 | // errType is a custom error type. 66 | type errType string 67 | 68 | func (e errType) Error() string { 69 | return string(e) 70 | } 71 | 72 | func TestEqual(t *testing.T) { 73 | t.Run("equal", func(t *testing.T) { 74 | now := time.Now() 75 | val := 42 76 | 77 | testCases := map[string]struct { 78 | got any 79 | want any 80 | }{ 81 | "integer": {got: 42, want: 42}, 82 | "string": {got: "hello", want: "hello"}, 83 | "bool": {got: true, want: true}, 84 | "struct": {got: intType{42}, want: intType{42}}, 85 | "pointer": {got: &val, want: &val}, 86 | "nil slice": {got: []int(nil), want: []int(nil)}, 87 | "byte slice": {got: []byte("abc"), want: []byte("abc")}, 88 | "int slice": {got: []int{42, 84}, want: []int{42, 84}}, 89 | "time.Time": {got: now, want: now}, 90 | "nil": {got: nil, want: nil}, 91 | "nil pointer": {got: (*int)(nil), want: (*int)(nil)}, 92 | "nil map": {got: map[string]int(nil), want: map[string]int(nil)}, 93 | "nil chan": {got: (chan int)(nil), want: (chan int)(nil)}, 94 | "empty map": {got: map[string]int{}, want: map[string]int{}}, 95 | "map": {got: map[string]int{"a": 42}, want: map[string]int{"a": 42}}, 96 | } 97 | 98 | for name, tc := range testCases { 99 | t.Run(name, func(t *testing.T) { 100 | tb := &mockTB{} 101 | be.Equal(tb, tc.got, tc.want) 102 | if tb.failed { 103 | t.Errorf("%#v vs %#v: should have passed", tc.got, tc.want) 104 | } 105 | }) 106 | } 107 | }) 108 | t.Run("non-equal", func(t *testing.T) { 109 | val1, val2 := 42, 84 110 | now := time.Now() 111 | 112 | testCases := map[string]struct { 113 | got any 114 | want any 115 | msg string 116 | }{ 117 | "integer": { 118 | got: 42, want: 84, 119 | msg: "got: 42; want: 84", 120 | }, 121 | "int32 vs int64": { 122 | got: int32(42), want: int64(42), 123 | msg: "got: 42; want: 42", 124 | }, 125 | "int vs string": { 126 | got: 42, want: "42", 127 | msg: `got: 42; want: "42"`, 128 | }, 129 | "string": { 130 | got: "hello", want: "world", 131 | msg: `got: "hello"; want: "world"`, 132 | }, 133 | "bool": { 134 | got: true, want: false, 135 | msg: "got: true; want: false", 136 | }, 137 | "struct": { 138 | got: intType{42}, want: intType{84}, 139 | msg: "got: be_test.intType{val:42}; want: be_test.intType{val:84}", 140 | }, 141 | "pointer": { 142 | got: &val1, want: &val2, 143 | }, 144 | "byte slice": { 145 | got: []byte("abc"), want: []byte("abd"), 146 | msg: `got: []byte{0x61, 0x62, 0x63}; want: []byte{0x61, 0x62, 0x64}`, 147 | }, 148 | "int slice": { 149 | got: []int{42, 84}, want: []int{84, 42}, 150 | msg: `got: []int{42, 84}; want: []int{84, 42}`, 151 | }, 152 | "int slice vs any slice": { 153 | got: []int{42, 84}, want: []any{42, 84}, 154 | msg: `got: []int{42, 84}; want: []interface {}{42, 84}`, 155 | }, 156 | "time.Time": { 157 | got: now, want: now.Add(time.Second), 158 | }, 159 | "nil vs non-nil": { 160 | got: nil, want: 42, 161 | msg: "got: ; want: 42", 162 | }, 163 | "non-nil vs nil": { 164 | got: 42, want: nil, 165 | msg: "got: 42; want: ", 166 | }, 167 | "nil vs empty": { 168 | got: []int(nil), want: []int{}, 169 | msg: "got: []int(nil); want: []int{}", 170 | }, 171 | "map": { 172 | got: map[string]int{"a": 42}, want: map[string]int{"a": 84}, 173 | msg: `got: map[string]int{"a":42}; want: map[string]int{"a":84}`, 174 | }, 175 | "chan": { 176 | got: make(chan int), want: make(chan int), 177 | }, 178 | } 179 | 180 | for name, tc := range testCases { 181 | t.Run(name, func(t *testing.T) { 182 | tb := &mockTB{} 183 | be.Equal(tb, tc.got, tc.want) 184 | if !tb.failed { 185 | t.Errorf("%#v vs %#v: should have failed", tc.got, tc.want) 186 | } 187 | if tb.fatal { 188 | t.Error("should not be fatal") 189 | } 190 | if tc.msg != "" && tb.msg != tc.msg { 191 | t.Errorf("got: %q; want: %q", tb.msg, tc.msg) 192 | } 193 | }) 194 | } 195 | }) 196 | t.Run("time", func(t *testing.T) { 197 | // date1 and date2 represent the same point in time, 198 | date1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) 199 | date2 := time.Date(2025, 1, 1, 5, 0, 0, 0, time.FixedZone("UTC+5", 5*3600)) 200 | tb := &mockTB{} 201 | be.Equal(tb, date1, date2) 202 | if tb.failed { 203 | t.Errorf("%#v vs %#v: should have passed", date1, date2) 204 | } 205 | }) 206 | t.Run("equaler", func(t *testing.T) { 207 | t.Run("equal", func(t *testing.T) { 208 | tb := &mockTB{} 209 | n1, n2 := newNoisy(42), newNoisy(42) 210 | be.Equal(tb, n1, n2) 211 | if tb.failed { 212 | t.Errorf("%#v vs %#v: should have passed", n1, n2) 213 | } 214 | }) 215 | t.Run("non-equal", func(t *testing.T) { 216 | tb := &mockTB{} 217 | n1, n2 := newNoisy(42), newNoisy(84) 218 | be.Equal(tb, n1, n2) 219 | if !tb.failed { 220 | t.Errorf("%#v vs %#v: should have failed", n1, n2) 221 | } 222 | if tb.fatal { 223 | t.Error("should not be fatal") 224 | } 225 | }) 226 | }) 227 | t.Run("no wants", func(t *testing.T) { 228 | tb := &mockTB{} 229 | be.Equal(tb, 42) 230 | if !tb.failed { 231 | t.Error("should have failed") 232 | } 233 | if !tb.fatal { 234 | t.Error("should be fatal") 235 | } 236 | wantMsg := "no wants given" 237 | if tb.msg != wantMsg { 238 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 239 | } 240 | }) 241 | t.Run("multiple wants", func(t *testing.T) { 242 | t.Run("all equal", func(t *testing.T) { 243 | tb := &mockTB{} 244 | x := 2 * 3 * 7 245 | be.Equal(tb, x, 42, 42, 42) 246 | if tb.failed { 247 | t.Error("should have passed") 248 | } 249 | }) 250 | t.Run("some equal", func(t *testing.T) { 251 | tb := &mockTB{} 252 | x := 2 * 3 * 7 253 | be.Equal(tb, x, 21, 42, 84) 254 | if tb.failed { 255 | t.Error("should have passed") 256 | } 257 | }) 258 | t.Run("none equal", func(t *testing.T) { 259 | tb := &mockTB{} 260 | x := 2 * 3 * 7 261 | be.Equal(tb, x, 11, 12, 13) 262 | if !tb.failed { 263 | t.Error("should have failed") 264 | } 265 | if tb.fatal { 266 | t.Error("should not be fatal") 267 | } 268 | wantMsg := "got: 42; want any of: [11 12 13]" 269 | if tb.msg != wantMsg { 270 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 271 | } 272 | }) 273 | }) 274 | } 275 | 276 | func TestErr(t *testing.T) { 277 | t.Run("want nil", func(t *testing.T) { 278 | t.Run("got nil", func(t *testing.T) { 279 | tb := &mockTB{} 280 | be.Err(tb, nil, nil) 281 | if tb.failed { 282 | t.Errorf("failed: %s", tb.msg) 283 | } 284 | }) 285 | t.Run("got error", func(t *testing.T) { 286 | tb := &mockTB{} 287 | err := errors.New("oops") 288 | be.Err(tb, err, nil) 289 | if !tb.failed { 290 | t.Error("should have failed") 291 | } 292 | if !tb.fatal { 293 | t.Error("should be fatal") 294 | } 295 | wantMsg := "unexpected error: oops" 296 | if tb.msg != wantMsg { 297 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 298 | } 299 | }) 300 | }) 301 | t.Run("want error", func(t *testing.T) { 302 | t.Run("got nil", func(t *testing.T) { 303 | tb := &mockTB{} 304 | err := errors.New("oops") 305 | be.Err(tb, nil, err) 306 | if !tb.failed { 307 | t.Error("should have failed") 308 | } 309 | if tb.fatal { 310 | t.Error("should not be fatal") 311 | } 312 | wantMsg := `got: ; want: error` 313 | if tb.msg != wantMsg { 314 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 315 | } 316 | }) 317 | t.Run("same error", func(t *testing.T) { 318 | tb := &mockTB{} 319 | err := errors.New("oops") 320 | be.Err(tb, err, err) 321 | if tb.failed { 322 | t.Errorf("failed: %s", tb.msg) 323 | } 324 | }) 325 | t.Run("wrapped error", func(t *testing.T) { 326 | tb := &mockTB{} 327 | err := errors.New("oops") 328 | wrappedErr := fmt.Errorf("wrapped: %w", err) 329 | be.Err(tb, wrappedErr, err) 330 | if tb.failed { 331 | t.Errorf("failed: %s", tb.msg) 332 | } 333 | }) 334 | t.Run("different value", func(t *testing.T) { 335 | tb := &mockTB{} 336 | err1 := errors.New("error 1") 337 | err2 := errors.New("error 2") 338 | be.Err(tb, err1, err2) 339 | if !tb.failed { 340 | t.Error("should have failed") 341 | } 342 | if tb.fatal { 343 | t.Error("should not be fatal") 344 | } 345 | wantMsg := "got: *errors.errorString(error 1); want: *errors.errorString(error 2)" 346 | if tb.msg != wantMsg { 347 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 348 | } 349 | }) 350 | t.Run("different type", func(t *testing.T) { 351 | tb := &mockTB{} 352 | err1 := errors.New("oops") 353 | err2 := errType("oops") 354 | be.Err(tb, err1, err2) 355 | if !tb.failed { 356 | t.Error("should have failed") 357 | } 358 | if tb.fatal { 359 | t.Error("should not be fatal") 360 | } 361 | wantMsg := "got: *errors.errorString(oops); want: be_test.errType(oops)" 362 | if tb.msg != wantMsg { 363 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 364 | } 365 | }) 366 | }) 367 | t.Run("want string", func(t *testing.T) { 368 | t.Run("contains", func(t *testing.T) { 369 | tb := &mockTB{} 370 | err := errors.New("the night is dark") 371 | be.Err(tb, err, "night is") 372 | if tb.failed { 373 | t.Errorf("failed: %s", tb.msg) 374 | } 375 | }) 376 | t.Run("does not contain", func(t *testing.T) { 377 | tb := &mockTB{} 378 | err := errors.New("the night is dark") 379 | be.Err(tb, err, "day") 380 | if !tb.failed { 381 | t.Error("should have failed") 382 | } 383 | if tb.fatal { 384 | t.Error("should not be fatal") 385 | } 386 | wantMsg := `got: "the night is dark"; want: "day"` 387 | if tb.msg != wantMsg { 388 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 389 | } 390 | }) 391 | }) 392 | t.Run("want type", func(t *testing.T) { 393 | t.Run("same type", func(t *testing.T) { 394 | tb := &mockTB{} 395 | err := errType("oops") 396 | be.Err(tb, err, reflect.TypeFor[errType]()) 397 | if tb.failed { 398 | t.Errorf("failed: %s", tb.msg) 399 | } 400 | }) 401 | t.Run("different type", func(t *testing.T) { 402 | tb := &mockTB{} 403 | err := errType("oops") 404 | be.Err(tb, err, reflect.TypeFor[*fs.PathError]()) 405 | if !tb.failed { 406 | t.Error("should have failed") 407 | } 408 | if tb.fatal { 409 | t.Error("should not be fatal") 410 | } 411 | wantMsg := "got: be_test.errType; want: *fs.PathError" 412 | if tb.msg != wantMsg { 413 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 414 | } 415 | }) 416 | }) 417 | t.Run("unsupported want", func(t *testing.T) { 418 | tb := &mockTB{} 419 | var want int 420 | be.Err(tb, errors.New("oops"), want) 421 | if !tb.failed { 422 | t.Error("should have failed") 423 | } 424 | if tb.fatal { 425 | t.Error("should not be fatal") 426 | } 427 | wantMsg := "unsupported want type: int" 428 | if tb.msg != wantMsg { 429 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 430 | } 431 | }) 432 | t.Run("no wants", func(t *testing.T) { 433 | t.Run("got error", func(t *testing.T) { 434 | tb := &mockTB{} 435 | err := errors.New("oops") 436 | be.Err(tb, err) 437 | if tb.failed { 438 | t.Error("should have passed") 439 | } 440 | 441 | }) 442 | t.Run("got nil", func(t *testing.T) { 443 | tb := &mockTB{} 444 | var err error 445 | be.Err(tb, err) 446 | if !tb.failed { 447 | t.Error("should have failed") 448 | } 449 | if tb.fatal { 450 | t.Error("should not be fatal") 451 | } 452 | wantMsg := "got: ; want: error" 453 | if tb.msg != wantMsg { 454 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 455 | } 456 | }) 457 | }) 458 | t.Run("multiple wants", func(t *testing.T) { 459 | t.Run("all match", func(t *testing.T) { 460 | tb := &mockTB{} 461 | err := errType("oops") 462 | be.Err(tb, err, errType("oops"), "oops", reflect.TypeFor[errType]()) 463 | if tb.failed { 464 | t.Error("should have passed") 465 | } 466 | }) 467 | t.Run("some match", func(t *testing.T) { 468 | tb := &mockTB{} 469 | err := errType("oops") 470 | be.Err(tb, err, errType("oops"), 42, reflect.TypeFor[errType]()) 471 | if tb.failed { 472 | t.Error("should have passed") 473 | } 474 | }) 475 | t.Run("none match", func(t *testing.T) { 476 | tb := &mockTB{} 477 | err := errType("oops") 478 | be.Err(tb, err, errType("failed"), 42, reflect.TypeFor[*fs.PathError]()) 479 | if !tb.failed { 480 | t.Error("should have failed") 481 | } 482 | if tb.fatal { 483 | t.Error("should not be fatal") 484 | } 485 | wantMsg := "got: be_test.errType(oops); want any of: [failed 42 *fs.PathError]" 486 | if tb.msg != wantMsg { 487 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 488 | } 489 | }) 490 | }) 491 | } 492 | 493 | func TestTrue(t *testing.T) { 494 | t.Run("true", func(t *testing.T) { 495 | tb := &mockTB{} 496 | be.True(tb, true) 497 | if tb.failed { 498 | t.Errorf("failed: %s", tb.msg) 499 | } 500 | }) 501 | t.Run("false", func(t *testing.T) { 502 | tb := &mockTB{} 503 | be.True(tb, false) 504 | if !tb.failed { 505 | t.Error("should have failed") 506 | } 507 | if tb.fatal { 508 | t.Error("should not be fatal") 509 | } 510 | wantMsg := "got: false; want: true" 511 | if tb.msg != wantMsg { 512 | t.Errorf("got: %q; want: %q", tb.msg, wantMsg) 513 | } 514 | }) 515 | t.Run("expression", func(t *testing.T) { 516 | tb := &mockTB{} 517 | f := func() int { return 42 } 518 | be.True(tb, (f() == 42)) 519 | if tb.failed { 520 | t.Errorf("failed: %s", tb.msg) 521 | } 522 | }) 523 | } 524 | --------------------------------------------------------------------------------