Skip to content

Commit f7a8b38

Browse files
committed
Add be.AssignedAs and deprecate 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 f7a8b38

File tree

4 files changed

+177
-5
lines changed

4 files changed

+177
-5
lines changed

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

+50
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)
@@ -287,6 +333,10 @@ func isNil(v any) bool {
287333
}
288334

289335
// Panic asserts that the given function panics when invoked.
336+
//
337+
// Deprecated: It is better to defer a call to recover with a more specific
338+
// assertion about the value of a panic, rather than only asserting that a
339+
// function panicked.
290340
func Panic(f func()) (result ghost.Result) {
291341
args := ghostlib.ArgsFromAST(f)
292342
argF := args[0]

be/assertions_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,110 @@
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(result.Message, `new(bytes.Buffer) (*bytes.Buffer) was assigned to new(io.Reader) (*io.Reader)
64+
value: `))
65+
})
66+
67+
t.Run("interface invalid", func(t *testing.T) {
68+
g := ghost.New(t)
69+
70+
var got any = 15
71+
var want io.Reader
72+
73+
result := be.AssignedAs(got, &want)
74+
g.Should(be.False(result.Ok))
75+
g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*io.Reader)
76+
value: 15`))
77+
78+
result = be.AssignedAs(15, new(io.Reader))
79+
g.Should(be.False(result.Ok))
80+
g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(io.Reader) (*io.Reader)
81+
value: 15`))
82+
})
83+
84+
t.Run("nil target", func(t *testing.T) {
85+
g := ghost.New(t)
86+
87+
var got any = 15
88+
var want *int
89+
90+
result := be.AssignedAs(got, want)
91+
g.Should(be.False(result.Ok))
92+
g.Should(be.Equal(result.Message, "target want cannot be nil"))
93+
})
94+
95+
t.Run("panic", func(t *testing.T) {
96+
g := ghost.New(t)
97+
98+
defer func() {
99+
var err error
100+
g.Must(be.AssignedAs(recover(), &err))
101+
g.Should(be.ErrorEqual(err, "oops"))
102+
}()
103+
104+
panic(errors.New("oops"))
105+
})
106+
}
107+
12108
func TestClose(t *testing.T) {
13109
t.Run("in delta", func(t *testing.T) {
14110
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)

0 commit comments

Comments
 (0)