Skip to content

Commit d90ad51

Browse files
committed
Add be.AssignedAs and remove be.Panic
I don't think the current panic assertion is very good, because asserting _that_ something panics is a lot less effective than asserting the _value_ of a panic. So this refactor makes it so you have to orchestrate the panic recovery yourself, but also makes it easier to assert something of type `any` with a more specific assertion. And generally I think `be.AssignedAs` is a smart assertion to have. It roughly resembles the shape of `be.ErrorAs`, so it shouldn't be very surprising.
1 parent 0adc681 commit d90ad51

File tree

7 files changed

+177
-85
lines changed

7 files changed

+177
-85
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ jobs:
4747
- name: Lint
4848
uses: golangci/golangci-lint-action@v3
4949
with:
50-
version: v1.57.2
50+
version: v1.62.2

.golangci.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ linters:
2020
- errchkjson
2121
- errname
2222
- errorlint
23-
- exportloopref
2423
- gocognit
2524
- gocritic
2625
- gofumpt

README.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ g.Should(be.DeepEqual([]string{"a", "b"}, []string{"a", "b"}))
109109
g.Should(be.SliceContaining([]int{1, 2, 3}, 2))
110110
g.Should(be.StringContaining("foobar", "foo"))
111111

112-
g.Should(be.Panic(func() { panic("oh no") }))
113-
114112
var err error
115113
g.NoError(err)
116114
g.Must(be.Nil(err))
@@ -203,6 +201,24 @@ g.Should(BeThirteen(myInt)) // "myInt is 0"
203201
g.Should(BeThirteen(5 + 6)) // "5 + 6 is 11"
204202
```
205203

204+
#### Handling Panics
205+
206+
If you expect your code to panic, it is better to assert that the value passed
207+
to `panic` has the properties you expect, rather than to make an assumption
208+
that the panic you encountered is the panic you were expecting. Ghost can be
209+
combined with `defer`/`recover` to access the full expressiveness of test
210+
assertions:
211+
212+
```go
213+
defer func() {
214+
var err error
215+
g.Must(be.AssignedAs(recover(), &err))
216+
g.Should(be.ErrorEqual(err, "a specific error occurred"))
217+
}()
218+
219+
doStuff()
220+
```
221+
206222
## Philosophy
207223

208224
### Ghost Does Assertions

be/assertions.go

+46-40
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,52 @@ import (
1414
"github.com/rliebz/ghost/internal/constraints"
1515
)
1616

17+
// AssignedAs assigns a value to a target of an arbitrary type.
18+
//
19+
// The target must be a non-nil pointer.
20+
func AssignedAs[T any](value any, target *T) ghost.Result {
21+
args := ghostlib.ArgsFromAST(value, target)
22+
argValue, argTarget := args[0], args[1]
23+
24+
if target == nil {
25+
return ghost.Result{
26+
Ok: false,
27+
Message: fmt.Sprintf("target %s cannot be nil", argTarget),
28+
}
29+
}
30+
31+
typedValue, ok := value.(T)
32+
if !ok {
33+
return ghost.Result{
34+
Ok: false,
35+
Message: fmt.Sprintf(
36+
`%s (%T) could not be assigned to %s (%T)
37+
value: %v`,
38+
argValue,
39+
value,
40+
argTarget,
41+
target,
42+
value,
43+
),
44+
}
45+
}
46+
47+
*target = typedValue
48+
49+
return ghost.Result{
50+
Ok: true,
51+
Message: fmt.Sprintf(
52+
`%s (%T) was assigned to %s (%T)
53+
value: %v`,
54+
argValue,
55+
value,
56+
argTarget,
57+
target,
58+
value,
59+
),
60+
}
61+
}
62+
1763
// Close asserts that a value is within a delta of another.
1864
func Close[T constraints.Integer | constraints.Float](got, want, delta T) ghost.Result {
1965
args := ghostlib.ArgsFromAST(got, want, delta)
@@ -286,46 +332,6 @@ func isNil(v any) bool {
286332
return false
287333
}
288334

289-
// Panic asserts that the given function panics when invoked.
290-
func Panic(f func()) (result ghost.Result) {
291-
args := ghostlib.ArgsFromAST(f)
292-
argF := args[0]
293-
294-
defer func() {
295-
if r := recover(); r != nil {
296-
if strings.Contains(argF, "\n") {
297-
result = ghost.Result{
298-
Ok: true,
299-
Message: fmt.Sprintf(`function panicked with value: %v
300-
%v
301-
`, r, argF),
302-
}
303-
} else {
304-
result = ghost.Result{
305-
Ok: true,
306-
Message: fmt.Sprintf(`function %v panicked with value: %v`, argF, r),
307-
}
308-
}
309-
}
310-
}()
311-
312-
f()
313-
314-
if strings.Contains(argF, "\n") {
315-
return ghost.Result{
316-
Ok: false,
317-
Message: fmt.Sprintf(`function did not panic
318-
%v
319-
`, argF),
320-
}
321-
}
322-
323-
return ghost.Result{
324-
Ok: false,
325-
Message: fmt.Sprintf("function %v did not panic", argF),
326-
}
327-
}
328-
329335
// SliceContaining asserts that an element exists in a given slice.
330336
func SliceContaining[T comparable](slice []T, element T) ghost.Result {
331337
args := ghostlib.ArgsFromAST(slice, element)

be/assertions_test.go

+98-37
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,112 @@
11
package be_test
22

33
import (
4+
"bytes"
45
"errors"
6+
"io"
57
"strings"
68
"testing"
79

810
"github.com/rliebz/ghost"
911
"github.com/rliebz/ghost/be"
1012
)
1113

14+
func TestAssignedAs(t *testing.T) {
15+
t.Run("primitive valid", func(t *testing.T) {
16+
g := ghost.New(t)
17+
18+
var got any = "some-value"
19+
var want string
20+
21+
result := be.AssignedAs(got, &want)
22+
g.Should(be.True(result.Ok))
23+
g.Should(be.Equal(result.Message, `got (string) was assigned to &want (*string)
24+
value: some-value`))
25+
26+
result = be.AssignedAs("some-value", new(string))
27+
g.Should(be.True(result.Ok))
28+
g.Should(be.Equal(result.Message, `"some-value" (string) was assigned to new(string) (*string)
29+
value: some-value`))
30+
})
31+
32+
t.Run("primitive invalid", func(t *testing.T) {
33+
g := ghost.New(t)
34+
35+
var got any = 15
36+
var want string
37+
38+
result := be.AssignedAs(got, &want)
39+
g.Should(be.False(result.Ok))
40+
g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*string)
41+
value: 15`))
42+
43+
result = be.AssignedAs(15, new(string))
44+
g.Should(be.False(result.Ok))
45+
g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(string) (*string)
46+
value: 15`,
47+
))
48+
})
49+
50+
t.Run("interface valid", func(t *testing.T) {
51+
g := ghost.New(t)
52+
53+
var got any = new(bytes.Buffer)
54+
var want io.Reader
55+
56+
result := be.AssignedAs(got, &want)
57+
g.Should(be.True(result.Ok))
58+
g.Should(be.Equal(result.Message, `got (*bytes.Buffer) was assigned to &want (*io.Reader)
59+
value: `))
60+
61+
result = be.AssignedAs(new(bytes.Buffer), new(io.Reader))
62+
g.Should(be.True(result.Ok))
63+
g.Should(be.Equal(
64+
result.Message,
65+
`new(bytes.Buffer) (*bytes.Buffer) was assigned to new(io.Reader) (*io.Reader)
66+
value: `))
67+
})
68+
69+
t.Run("interface invalid", func(t *testing.T) {
70+
g := ghost.New(t)
71+
72+
var got any = 15
73+
var want io.Reader
74+
75+
result := be.AssignedAs(got, &want)
76+
g.Should(be.False(result.Ok))
77+
g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*io.Reader)
78+
value: 15`))
79+
80+
result = be.AssignedAs(15, new(io.Reader))
81+
g.Should(be.False(result.Ok))
82+
g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(io.Reader) (*io.Reader)
83+
value: 15`))
84+
})
85+
86+
t.Run("nil target", func(t *testing.T) {
87+
g := ghost.New(t)
88+
89+
var got any = 15
90+
var want *int
91+
92+
result := be.AssignedAs(got, want)
93+
g.Should(be.False(result.Ok))
94+
g.Should(be.Equal(result.Message, "target want cannot be nil"))
95+
})
96+
97+
t.Run("panic", func(t *testing.T) {
98+
g := ghost.New(t)
99+
100+
defer func() {
101+
var err error
102+
g.Must(be.AssignedAs(recover(), &err))
103+
g.Should(be.ErrorEqual(err, "oops"))
104+
}()
105+
106+
panic(errors.New("oops"))
107+
})
108+
}
109+
12110
func TestClose(t *testing.T) {
13111
t.Run("in delta", func(t *testing.T) {
14112
g := ghost.New(t)
@@ -557,43 +655,6 @@ func TestNil(t *testing.T) {
557655
})
558656
}
559657

560-
func TestPanic(t *testing.T) {
561-
t.Run("panic", func(t *testing.T) {
562-
g := ghost.New(t)
563-
564-
f := func() { panic(errors.New("oh no")) }
565-
566-
result := be.Panic(f)
567-
g.Should(be.True(result.Ok))
568-
g.Should(be.Equal(result.Message, "function f panicked with value: oh no"))
569-
570-
result = be.Panic(func() { panic(errors.New("oh no")) })
571-
g.Should(be.True(result.Ok))
572-
g.Should(be.Equal(result.Message, `function panicked with value: oh no
573-
func() {
574-
panic(errors.New("oh no"))
575-
}
576-
`))
577-
})
578-
579-
t.Run("no panic", func(t *testing.T) {
580-
g := ghost.New(t)
581-
582-
f := func() {}
583-
584-
result := be.Panic(f)
585-
g.Should(be.False(result.Ok))
586-
g.Should(be.Equal(result.Message, "function f did not panic"))
587-
588-
result = be.Panic(func() {})
589-
g.Should(be.False(result.Ok))
590-
g.Should(be.Equal(result.Message, `function did not panic
591-
func() {
592-
}
593-
`))
594-
})
595-
}
596-
597658
func TestSliceContaining(t *testing.T) {
598659
t.Run("contains <= 3", func(t *testing.T) {
599660
g := ghost.New(t)

example_test.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ func TestExample(t *testing.T) {
2727

2828
g.Should(be.StringContaining("foobar", "foo"))
2929

30-
g.Should(be.Panic(func() { panic("oh no") }))
31-
g.ShouldNot(be.Panic(func() {}))
32-
3330
var err error
3431
g.NoError(err)
3532
g.Must(be.Nil(err))
@@ -44,6 +41,19 @@ func TestExample(t *testing.T) {
4441
g.ShouldNot(be.JSONEqual(`{"a":1}`, `{"a":2}`))
4542
}
4643

44+
func ExampleAssignedAs() {
45+
t := new(testing.T) // from the test
46+
g := ghost.New(t)
47+
48+
defer func() {
49+
var err error
50+
g.Must(be.AssignedAs(recover(), &err))
51+
g.Should(be.ErrorEqual(err, "oops"))
52+
}()
53+
54+
panic(errors.New("oops"))
55+
}
56+
4757
func ExampleEventually() {
4858
t := new(testing.T) // from the test
4959
g := ghost.New(t)

tusk.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ tasks:
99
usage: Run static analysis
1010
description: |
1111
Run golangci-lint using the project configuration.
12-
run: go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1 run ./...
12+
run: go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./...
1313

1414
test:
1515
usage: Run unit tests

0 commit comments

Comments
 (0)