Repository: stretchr/testify
Pull request #1753 (2025-05-28 16:36:16)

assert.Empty: comprehensive doc of "Empty"-ness rules
## Summary

Document the [`assert.Empty`](https://pkg.go.dev/github.com/stretchr/testify/assert#Empty) rules more comprehensively. This exposes our quirks to the user to avoid wrong expectations.

Add many many many more test cases that document edges cases and will allow to catch breaking changes and avoid regressions.

## Changes
* improve doc of `assert.Empty`, `assert.NotEmpty` (and their generated childs in package `require`)
* add many test cases for the internal `isEmpty`. They are added in the current style for now, but I have plans for a future refactoring (using table tests).

## Motivation

[`assert.Empty`](https://pkg.go.dev/github.com/stretchr/testify/assert#Empty) has quirks which desserve to be documented for our users, and for contributors.

The extensive test suite will help to ease future refactorings of Testify internals by catch breaking changes. 

## Related issues
<!-- Put `Closes #XXXX` for each issue number this PR fixes/closes -->


dolmen: Cc: @PeterEFinch could you review this?
ccoVeille: I learned things BTW

Patch:
diff --git a/assert/assertion_format.go b/assert/assertion_format.go
index 7144041..c592f6a 100644
--- a/assert/assertion_format.go
+++ b/assert/assertion_format.go
@@ -50,10 +50,19 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string
 	return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...)
 }
 
-// Emptyf asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Emptyf asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	assert.Emptyf(t, obj, "error message %s", "formatted")
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
 	if h, ok := t.(tHelper); ok {
 		h.Helper()
@@ -595,8 +604,7 @@ func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg str
 	return NotElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...)
 }
 
-// NotEmptyf asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmptyf asserts that the specified object is NOT [Empty].
 //
 //	if assert.NotEmptyf(t, obj, "error message %s", "formatted") {
 //	  assert.Equal(t, "two", obj[1])
diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go
index 38e253e..58db928 100644
--- a/assert/assertion_forward.go
+++ b/assert/assertion_forward.go
@@ -92,10 +92,19 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st
 	return ElementsMatchf(a.t, listA, listB, msg, args...)
 }
 
-// Empty asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Empty asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	a.Empty(obj)
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool {
 	if h, ok := a.t.(tHelper); ok {
 		h.Helper()
@@ -103,10 +112,19 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool {
 	return Empty(a.t, object, msgAndArgs...)
 }
 
-// Emptyf asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Emptyf asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	a.Emptyf(obj, "error message %s", "formatted")
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool {
 	if h, ok := a.t.(tHelper); ok {
 		h.Helper()
@@ -1182,8 +1200,7 @@ func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg
 	return NotElementsMatchf(a.t, listA, listB, msg, args...)
 }
 
-// NotEmpty asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmpty asserts that the specified object is NOT [Empty].
 //
 //	if a.NotEmpty(obj) {
 //	  assert.Equal(t, "two", obj[1])
@@ -1195,8 +1212,7 @@ func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) boo
 	return NotEmpty(a.t, object, msgAndArgs...)
 }
 
-// NotEmptyf asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmptyf asserts that the specified object is NOT [Empty].
 //
 //	if a.NotEmptyf(obj, "error message %s", "formatted") {
 //	  assert.Equal(t, "two", obj[1])
diff --git a/assert/assertions.go b/assert/assertions.go
index 9913ac2..4658206 100644
--- a/assert/assertions.go
+++ b/assert/assertions.go
@@ -773,10 +773,19 @@ func isEmpty(object interface{}) bool {
 	}
 }
 
-// Empty asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Empty asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	assert.Empty(t, obj)
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
 	pass := isEmpty(object)
 	if !pass {
@@ -789,8 +798,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
 	return pass
 }
 
-// NotEmpty asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmpty asserts that the specified object is NOT [Empty].
 //
 //	if assert.NotEmpty(t, obj) {
 //	  assert.Equal(t, "two", obj[1])
diff --git a/require/require.go b/require/require.go
index 6cd1334..2d02f9b 100644
--- a/require/require.go
+++ b/require/require.go
@@ -117,10 +117,19 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string
 	t.FailNow()
 }
 
-// Empty asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Empty asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	require.Empty(t, obj)
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
 	if h, ok := t.(tHelper); ok {
 		h.Helper()
@@ -131,10 +140,19 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
 	t.FailNow()
 }
 
-// Emptyf asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Emptyf asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	require.Emptyf(t, obj, "error message %s", "formatted")
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) {
 	if h, ok := t.(tHelper); ok {
 		h.Helper()
@@ -1495,8 +1513,7 @@ func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg str
 	t.FailNow()
 }
 
-// NotEmpty asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmpty asserts that the specified object is NOT [Empty].
 //
 //	if require.NotEmpty(t, obj) {
 //	  require.Equal(t, "two", obj[1])
@@ -1511,8 +1528,7 @@ func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
 	t.FailNow()
 }
 
-// NotEmptyf asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmptyf asserts that the specified object is NOT [Empty].
 //
 //	if require.NotEmptyf(t, obj, "error message %s", "formatted") {
 //	  require.Equal(t, "two", obj[1])
diff --git a/require/require_forward.go b/require/require_forward.go
index f6192df..e6f7e94 100644
--- a/require/require_forward.go
+++ b/require/require_forward.go
@@ -93,10 +93,19 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st
 	ElementsMatchf(a.t, listA, listB, msg, args...)
 }
 
-// Empty asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Empty asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	a.Empty(obj)
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) {
 	if h, ok := a.t.(tHelper); ok {
 		h.Helper()
@@ -104,10 +113,19 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) {
 	Empty(a.t, object, msgAndArgs...)
 }
 
-// Emptyf asserts that the specified object is empty.  I.e. nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// Emptyf asserts that the given value is "empty".
+//
+// [Zero values] are "empty".
+//
+// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
+//
+// Slices, maps and channels with zero length are "empty".
+//
+// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
 //
 //	a.Emptyf(obj, "error message %s", "formatted")
+//
+// [Zero values]: https://go.dev/ref/spec#The_zero_value
 func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) {
 	if h, ok := a.t.(tHelper); ok {
 		h.Helper()
@@ -1183,8 +1201,7 @@ func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg
 	NotElementsMatchf(a.t, listA, listB, msg, args...)
 }
 
-// NotEmpty asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmpty asserts that the specified object is NOT [Empty].
 //
 //	if a.NotEmpty(obj) {
 //	  assert.Equal(t, "two", obj[1])
@@ -1196,8 +1213,7 @@ func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) {
 	NotEmpty(a.t, object, msgAndArgs...)
 }
 
-// NotEmptyf asserts that the specified object is NOT empty.  I.e. not nil, "", false, 0 or either
-// a slice or a channel with len == 0.
+// NotEmptyf asserts that the specified object is NOT [Empty].
 //
 //	if a.NotEmptyf(obj, "error message %s", "formatted") {
 //	  assert.Equal(t, "two", obj[1])


Test patch:
diff --git a/assert/assertions_test.go b/assert/assertions_test.go
index 955188d..d30eef6 100644
--- a/assert/assertions_test.go
+++ b/assert/assertions_test.go
@@ -1728,24 +1728,119 @@ func Test_isEmpty(t *testing.T) {
 
 	True(t, isEmpty(""))
 	True(t, isEmpty(nil))
+	True(t, isEmpty(error(nil)))
+	True(t, isEmpty((*int)(nil)))
+	True(t, isEmpty((*string)(nil)))
+	True(t, isEmpty(new(string)))
 	True(t, isEmpty([]string{}))
+	True(t, isEmpty([]string(nil)))
+	True(t, isEmpty([]byte(nil)))
+	True(t, isEmpty([]byte{}))
+	True(t, isEmpty([]byte("")))
+	True(t, isEmpty([]bool(nil)))
+	True(t, isEmpty([]bool{}))
+	True(t, isEmpty([]interface{}(nil)))
+	True(t, isEmpty([]interface{}{}))
+	True(t, isEmpty(struct{}{}))
+	True(t, isEmpty(&struct{}{}))
+	True(t, isEmpty(struct{ A int }{A: 0}))
+	True(t, isEmpty(struct{ a int }{a: 0}))
+	True(t, isEmpty(struct {
+		a int
+		B int
+	}{a: 0, B: 0}))
 	True(t, isEmpty(0))
+	True(t, isEmpty(int(0)))
+	True(t, isEmpty(int8(0)))
+	True(t, isEmpty(int16(0)))
+	True(t, isEmpty(uint16(0)))
 	True(t, isEmpty(int32(0)))
+	True(t, isEmpty(uint32(0)))
 	True(t, isEmpty(int64(0)))
+	True(t, isEmpty(uint64(0)))
+	True(t, isEmpty('\u0000')) // rune => int32
+	True(t, isEmpty(float32(0)))
+	True(t, isEmpty(float64(0)))
+	True(t, isEmpty(0i))   // complex
+	True(t, isEmpty(0.0i)) // complex
 	True(t, isEmpty(false))
+	True(t, isEmpty(new(bool)))
 	True(t, isEmpty(map[string]string{}))
+	True(t, isEmpty(map[string]string(nil)))
 	True(t, isEmpty(new(time.Time)))
 	True(t, isEmpty(time.Time{}))
 	True(t, isEmpty(make(chan struct{})))
-	True(t, isEmpty([1]int{}))
+	True(t, isEmpty(chan struct{}(nil)))
+	True(t, isEmpty(chan<- struct{}(nil)))
+	True(t, isEmpty(make(chan struct{})))
+	True(t, isEmpty(make(chan<- struct{})))
+	True(t, isEmpty(make(chan struct{}, 1)))
+	True(t, isEmpty(make(chan<- struct{}, 1)))
+	True(t, isEmpty([1]int{0}))
+	True(t, isEmpty([2]int{0, 0}))
+	True(t, isEmpty([8]int{}))
+	True(t, isEmpty([...]int{7: 0}))
+	True(t, isEmpty([...]bool{false, false}))
+	True(t, isEmpty(errors.New(""))) // BEWARE
+	True(t, isEmpty([]error{}))
+	True(t, isEmpty([]error(nil)))
+	True(t, isEmpty(&[1]int{0}))
+	True(t, isEmpty(&[2]int{0, 0}))
 	False(t, isEmpty("something"))
 	False(t, isEmpty(errors.New("something")))
 	False(t, isEmpty([]string{"something"}))
 	False(t, isEmpty(1))
+	False(t, isEmpty(int(1)))
+	False(t, isEmpty(uint(1)))
+	False(t, isEmpty(byte(1)))
+	False(t, isEmpty(int8(1)))
+	False(t, isEmpty(uint8(1)))
+	False(t, isEmpty(int16(1)))
+	False(t, isEmpty(uint16(1)))
+	False(t, isEmpty(int32(1)))
+	False(t, isEmpty(uint32(1)))
+	False(t, isEmpty(int64(1)))
+	False(t, isEmpty(uint64(1)))
+	False(t, isEmpty('A')) // rune => int32
 	False(t, isEmpty(true))
+	False(t, isEmpty(1.0))
+	False(t, isEmpty(1i))            // complex
+	False(t, isEmpty([]byte{0}))     // elements values are ignored for slices
+	False(t, isEmpty([]byte{0, 0}))  // elements values are ignored for slices
+	False(t, isEmpty([]string{""}))  // elements values are ignored for slices
+	False(t, isEmpty([]string{"a"})) // elements values are ignored for slices
+	False(t, isEmpty([]bool{false})) // elements values are ignored for slices
+	False(t, isEmpty([]bool{true}))  // elements values are ignored for slices
+	False(t, isEmpty([]error{errors.New("xxx")}))
+	False(t, isEmpty([]error{nil}))            // BEWARE
+	False(t, isEmpty([]error{errors.New("")})) // BEWARE
 	False(t, isEmpty(map[string]string{"Hello": "World"}))
+	False(t, isEmpty(map[string]string{"": ""}))
+	False(t, isEmpty(map[string]string{"foo": ""}))
+	False(t, isEmpty(map[string]string{"": "foo"}))
 	False(t, isEmpty(chWithValue))
+	False(t, isEmpty([1]bool{true}))
+	False(t, isEmpty([2]bool{false, true}))
+	False(t, isEmpty([...]bool{10: true}))
+	False(t, isEmpty([]int{0}))
+	False(t, isEmpty([]int{42}))
 	False(t, isEmpty([1]int{42}))
+	False(t, isEmpty([2]int{0, 42}))
+	False(t, isEmpty(&[1]int{42}))
+	False(t, isEmpty(&[2]int{0, 42}))
+	False(t, isEmpty([1]*int{new(int)})) // array elements must be the zero value, not any Empty value
+	False(t, isEmpty(struct{ A int }{A: 42}))
+	False(t, isEmpty(struct{ a int }{a: 42}))
+	False(t, isEmpty(struct{ a *int }{a: new(int)})) // fields must be the zero value, not any Empty value
+	False(t, isEmpty(struct{ a []int }{a: []int{}})) // fields must be the zero value, not any Empty value
+	False(t, isEmpty(struct {
+		a int
+		B int
+	}{a: 0, B: 42}))
+	False(t, isEmpty(struct {
+		a int
+		B int
+	}{a: 42, B: 0}))
 }
 
 func TestEmpty(t *testing.T) {