Skip to content

Commit ca82e58

Browse files
committed
feat: added Kind and NotKind to compare with reflect.Kind
This adapts stretchr#1803. We have adopted slightly different design choices: * we think checking for nil and reflect.Invalid is a legit use case * we prefer to use [reflect.Value.Kind] over [reflect.Type.Kind] so as to reflect the concrete type of the object. This way we don't have to handle the case when [reflect.TypeOf] is nil, which again may well be a legitimate assertion. Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 21cd9d4 commit ca82e58

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

internal/assertions/type.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,67 @@ func NotZero(t T, i any, msgAndArgs ...any) bool {
146146
return true
147147
}
148148

149+
// Kind asserts that the [reflect.Kind] of a given object matches the expected [reflect.Kind].
150+
//
151+
// Kind reflects the concrete value stored in the object. The nil value (or interface with nil value)
152+
// are comparable to [reflect.Invalid]. See also [reflect.Value.Kind].
153+
//
154+
// # Usage
155+
//
156+
// assertions.Kind(t, reflect.String, "Hello World")
157+
//
158+
// # Examples
159+
//
160+
// success: reflect.String, "hello"
161+
// failure: reflect.String, 0
162+
func Kind(t T, expectedKind reflect.Kind, object any, msgAndArgs ...any) bool {
163+
// Domain: type
164+
if h, ok := t.(H); ok {
165+
h.Helper()
166+
}
167+
168+
val := reflect.ValueOf(object)
169+
kind := val.Kind()
170+
if kind != expectedKind {
171+
if kind == reflect.Invalid {
172+
// add some explanation when reflect.Invalid does not match the expectation (common gotcha with reflect)
173+
return Fail(t, "object has reflect.Invalid kind: this is nil or an interface with nil value", msgAndArgs...)
174+
}
175+
176+
return Fail(t, fmt.Sprintf("object expected to be of kind %v, but was %v", expectedKind, kind), msgAndArgs...)
177+
}
178+
179+
return true
180+
}
181+
182+
// NotKind asserts that the [reflect.Kind] of a given object does not match the expected [reflect.Kind].
183+
//
184+
// Kind reflects the concrete value stored in the object. The nil value (or interface with nil value)
185+
// are comparable to [reflect.Invalid]. See also [reflect.Value.Kind].
186+
//
187+
// # Usage
188+
//
189+
// assertions.NotKind(t, reflect.Int, "Hello World")
190+
//
191+
// # Examples
192+
//
193+
// success: reflect.String, 0
194+
// failure: reflect.String, "hello"
195+
func NotKind(t T, expectedKind reflect.Kind, object any, msgAndArgs ...any) bool {
196+
// Domain: type
197+
if h, ok := t.(H); ok {
198+
h.Helper()
199+
}
200+
201+
val := reflect.ValueOf(object)
202+
kind := val.Kind()
203+
if kind != expectedKind {
204+
return true
205+
}
206+
207+
return Fail(t, fmt.Sprintf("object expected not to be of kind %v, but was %v", expectedKind, kind), msgAndArgs...)
208+
}
209+
149210
func isType(expectedType, object any) bool {
150211
return ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType))
151212
}

internal/assertions/type_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package assertions
55

66
import (
7+
"errors"
78
"iter"
9+
"reflect"
810
"slices"
911
"testing"
1012
)
@@ -101,6 +103,39 @@ func TestTypeNotZero(t *testing.T) {
101103
}
102104
}
103105

106+
func TestTypeKind(t *testing.T) {
107+
t.Parallel()
108+
109+
for tt := range kindCases() {
110+
t.Run(tt.name, func(t *testing.T) {
111+
t.Parallel()
112+
113+
mock := new(mockT)
114+
result := Kind(mock, tt.expectedKind, tt.value)
115+
resultNot := NotKind(mock, tt.expectedKind, tt.value)
116+
117+
if tt.result {
118+
if !result {
119+
t.Errorf("expected kind of %T to be %q, but Kind reported %t", tt.value, tt.expectedKind, result)
120+
}
121+
if resultNot {
122+
t.Errorf("expected kind of %T to be %q, but NotKind reported %t", tt.value, tt.expectedKind, resultNot)
123+
}
124+
125+
return
126+
}
127+
128+
// expected: false
129+
if result {
130+
t.Errorf("expected kind of %T NOT to be %q, but Kind reported %t", tt.value, tt.expectedKind, result)
131+
}
132+
if !resultNot {
133+
t.Errorf("expected kind of %T NOT to be %q, but NotKind reported %t", tt.value, tt.expectedKind, resultNot)
134+
}
135+
})
136+
}
137+
}
138+
104139
func TestTypeDiffEmptyCases(t *testing.T) {
105140
t.Parallel()
106141

@@ -216,3 +251,42 @@ func typeNonZeros() iter.Seq[any] {
216251
(chan<- any)(make(chan any)),
217252
})
218253
}
254+
255+
type kindCase struct {
256+
expectedKind reflect.Kind
257+
value any
258+
result bool
259+
name string
260+
}
261+
262+
func kindCases() iter.Seq[kindCase] {
263+
var iface any = "string"
264+
265+
return slices.Values([]kindCase{
266+
// True cases
267+
{reflect.String, "Hello World", true, "is string"},
268+
{reflect.Int, 123, true, "is int"},
269+
{reflect.Array, [6]int{2, 3, 5, 7, 11, 13}, true, "is array"},
270+
{reflect.Func, Kind, true, "is func"},
271+
{reflect.Float64, 0.0345, true, "is float64"},
272+
{reflect.Map, make(map[string]int), true, "is map"},
273+
{reflect.Bool, true, true, "is bool"},
274+
{reflect.Ptr, new(int), true, "is pointer"},
275+
// False cases
276+
{reflect.String, 13, false, "not string"},
277+
{reflect.Int, [6]int{2, 3, 5, 7, 11, 13}, false, "not int"},
278+
{reflect.Float64, 12, false, "not float64"},
279+
{reflect.Bool, make(map[string]int), false, "not bool"},
280+
// Edge cases
281+
// True
282+
{reflect.Invalid, any(nil), true, "legitimate expectation of reflect.Invalid (any)"},
283+
{reflect.Ptr, (*any)(nil), true, "legitimate expectation of reflect.Pointer (*any)"},
284+
{reflect.Invalid, (error)(nil), true, "legitimate expectation of reflect.Invalid (error)"},
285+
{reflect.Invalid, nil, true, "legitimate nil input"},
286+
// False
287+
{reflect.Interface, iface, false, "interface returns concrete type (any)"},
288+
{reflect.Interface, errors.New("stuff"), false, "interface returns concrete type (error)"},
289+
{reflect.Invalid, "string", false, "wrong expectation of reflect.Invalid"},
290+
{reflect.Ptr, nil, false, "nil input"},
291+
})
292+
}

0 commit comments

Comments
 (0)