mirror of https://github.com/stretchr/testify.git
EqualExportedValues: Handle nested pointer, slice and map fields (#1379)
* EqualExportedValues: Handle pointer and slice fields * Update assert/assertions.go Co-authored-by: Michael Pu <michael.pu123@gmail.com> * Reduce redundant calls to 'copyExportedFields' * Update comments * Add support for maps * Update Go version support to 1.19 and onward * Re-generate after rebasing --------- Co-authored-by: Michael Pu <michael.pu123@gmail.com>pull/1386/head v1.8.3
parent
4b2f4d2bcf
commit
4c93d8f201
|
@ -6,7 +6,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_version: ["1.18.1", "1.17.6", "1.16.5"]
|
||||
go_version: ["1.20", "1.19"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Go
|
||||
|
|
|
@ -348,7 +348,7 @@ To update Testify to the latest version, use `go get -u github.com/stretchr/test
|
|||
Supported go versions
|
||||
==================
|
||||
|
||||
We currently support the most recent major Go versions from 1.13 onward.
|
||||
We currently support the most recent major Go versions from 1.19 onward.
|
||||
|
||||
------
|
||||
|
||||
|
|
|
@ -75,46 +75,75 @@ func ObjectsAreEqual(expected, actual interface{}) bool {
|
|||
return bytes.Equal(exp, act)
|
||||
}
|
||||
|
||||
// ObjectsExportedFieldsAreEqual determines if the exported (public) fields of two structs are considered equal.
|
||||
// If the two objects are not of the same type, or if either of them are not a struct, they are not considered equal.
|
||||
//
|
||||
// This function does no assertion of any kind.
|
||||
func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return expected == actual
|
||||
// copyExportedFields iterates downward through nested data structures and creates a copy
|
||||
// that only contains the exported struct fields.
|
||||
func copyExportedFields(expected interface{}) interface{} {
|
||||
if isNil(expected) {
|
||||
return expected
|
||||
}
|
||||
|
||||
expectedType := reflect.TypeOf(expected)
|
||||
actualType := reflect.TypeOf(actual)
|
||||
|
||||
if expectedType != actualType {
|
||||
return false
|
||||
}
|
||||
|
||||
if expectedType.Kind() != reflect.Struct || actualType.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
expectedKind := expectedType.Kind()
|
||||
expectedValue := reflect.ValueOf(expected)
|
||||
actualValue := reflect.ValueOf(actual)
|
||||
|
||||
switch expectedKind {
|
||||
case reflect.Struct:
|
||||
result := reflect.New(expectedType).Elem()
|
||||
for i := 0; i < expectedType.NumField(); i++ {
|
||||
field := expectedType.Field(i)
|
||||
isExported := field.PkgPath == "" // should use field.IsExported() but it's not available in Go 1.16.5
|
||||
isExported := field.IsExported()
|
||||
if isExported {
|
||||
var equal bool
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
equal = ObjectsExportedFieldsAreEqual(expectedValue.Field(i).Interface(), actualValue.Field(i).Interface())
|
||||
} else {
|
||||
equal = ObjectsAreEqualValues(expectedValue.Field(i).Interface(), actualValue.Field(i).Interface())
|
||||
fieldValue := expectedValue.Field(i)
|
||||
if isNil(fieldValue) || isNil(fieldValue.Interface()) {
|
||||
continue
|
||||
}
|
||||
newValue := copyExportedFields(fieldValue.Interface())
|
||||
result.Field(i).Set(reflect.ValueOf(newValue))
|
||||
}
|
||||
}
|
||||
return result.Interface()
|
||||
|
||||
case reflect.Ptr:
|
||||
result := reflect.New(expectedType.Elem())
|
||||
unexportedRemoved := copyExportedFields(expectedValue.Elem().Interface())
|
||||
result.Elem().Set(reflect.ValueOf(unexportedRemoved))
|
||||
return result.Interface()
|
||||
|
||||
case reflect.Array, reflect.Slice:
|
||||
result := reflect.MakeSlice(expectedType, expectedValue.Len(), expectedValue.Len())
|
||||
for i := 0; i < expectedValue.Len(); i++ {
|
||||
index := expectedValue.Index(i)
|
||||
if isNil(index) {
|
||||
continue
|
||||
}
|
||||
unexportedRemoved := copyExportedFields(index.Interface())
|
||||
result.Index(i).Set(reflect.ValueOf(unexportedRemoved))
|
||||
}
|
||||
return result.Interface()
|
||||
|
||||
case reflect.Map:
|
||||
result := reflect.MakeMap(expectedType)
|
||||
for _, k := range expectedValue.MapKeys() {
|
||||
index := expectedValue.MapIndex(k)
|
||||
unexportedRemoved := copyExportedFields(index.Interface())
|
||||
result.SetMapIndex(k, reflect.ValueOf(unexportedRemoved))
|
||||
}
|
||||
return result.Interface()
|
||||
|
||||
default:
|
||||
return expected
|
||||
}
|
||||
}
|
||||
|
||||
if !equal {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
// ObjectsExportedFieldsAreEqual determines if the exported (public) fields of two objects are
|
||||
// considered equal. This comparison of only exported fields is applied recursively to nested data
|
||||
// structures.
|
||||
//
|
||||
// This function does no assertion of any kind.
|
||||
func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool {
|
||||
expectedCleaned := copyExportedFields(expected)
|
||||
actualCleaned := copyExportedFields(actual)
|
||||
return ObjectsAreEqualValues(expectedCleaned, actualCleaned)
|
||||
}
|
||||
|
||||
// ObjectsAreEqualValues gets whether two objects are equal, or if their
|
||||
|
@ -545,7 +574,10 @@ func EqualExportedValues(t TestingT, expected, actual interface{}, msgAndArgs ..
|
|||
return Fail(t, fmt.Sprintf("Types expected to both be struct \n\t%v != %v", bType.Kind(), reflect.Struct), msgAndArgs...)
|
||||
}
|
||||
|
||||
if !ObjectsExportedFieldsAreEqual(expected, actual) {
|
||||
expected = copyExportedFields(expected)
|
||||
actual = copyExportedFields(actual)
|
||||
|
||||
if !ObjectsAreEqualValues(expected, actual) {
|
||||
diff := diff(expected, actual)
|
||||
expected, actual = formatUnequalValues(expected, actual)
|
||||
return Fail(t, fmt.Sprintf("Not equal (comparing only exported fields): \n"+
|
||||
|
|
|
@ -150,7 +150,6 @@ func TestObjectsAreEqual(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestObjectsExportedFieldsAreEqual(t *testing.T) {
|
||||
type Nested struct {
|
||||
Exported interface{}
|
||||
notExported interface{}
|
||||
|
@ -167,6 +166,28 @@ func TestObjectsExportedFieldsAreEqual(t *testing.T) {
|
|||
foo interface{}
|
||||
}
|
||||
|
||||
type S3 struct {
|
||||
Exported1 *Nested
|
||||
Exported2 *Nested
|
||||
}
|
||||
|
||||
type S4 struct {
|
||||
Exported1 []*Nested
|
||||
}
|
||||
|
||||
type S5 struct {
|
||||
Exported Nested
|
||||
}
|
||||
|
||||
type S6 struct {
|
||||
Exported string
|
||||
unexported string
|
||||
}
|
||||
|
||||
func TestObjectsExportedFieldsAreEqual(t *testing.T) {
|
||||
|
||||
intValue := 1
|
||||
|
||||
cases := []struct {
|
||||
expected interface{}
|
||||
actual interface{}
|
||||
|
@ -181,6 +202,49 @@ func TestObjectsExportedFieldsAreEqual(t *testing.T) {
|
|||
{S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{"a", 3}, 4, Nested{5, 6}}, false},
|
||||
{S{1, Nested{2, 3}, 4, Nested{5, 6}}, S2{1}, false},
|
||||
{1, S{1, Nested{2, 3}, 4, Nested{5, 6}}, false},
|
||||
|
||||
{S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, 2}, &Nested{3, 4}}, true},
|
||||
{S3{nil, &Nested{3, 4}}, S3{nil, &Nested{3, 4}}, true},
|
||||
{S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, 2}, &Nested{3, "b"}}, true},
|
||||
{S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, "a"}, &Nested{3, "b"}}, true},
|
||||
{S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{"a", 2}, &Nested{3, 4}}, false},
|
||||
{S3{&Nested{1, 2}, &Nested{3, 4}}, S3{}, false},
|
||||
{S3{}, S3{}, true},
|
||||
|
||||
{S4{[]*Nested{{1, 2}}}, S4{[]*Nested{{1, 2}}}, true},
|
||||
{S4{[]*Nested{{1, 2}}}, S4{[]*Nested{{1, 3}}}, true},
|
||||
{S4{[]*Nested{{1, 2}, {3, 4}}}, S4{[]*Nested{{1, "a"}, {3, "b"}}}, true},
|
||||
{S4{[]*Nested{{1, 2}, {3, 4}}}, S4{[]*Nested{{1, "a"}, {2, "b"}}}, false},
|
||||
|
||||
{Nested{&intValue, 2}, Nested{&intValue, 2}, true},
|
||||
{Nested{&Nested{1, 2}, 3}, Nested{&Nested{1, "b"}, 3}, true},
|
||||
{Nested{&Nested{1, 2}, 3}, Nested{nil, 3}, false},
|
||||
|
||||
{
|
||||
Nested{map[interface{}]*Nested{nil: nil}, 2},
|
||||
Nested{map[interface{}]*Nested{nil: nil}, 2},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Nested{map[interface{}]*Nested{"a": nil}, 2},
|
||||
Nested{map[interface{}]*Nested{"a": nil}, 2},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Nested{map[interface{}]*Nested{"a": nil}, 2},
|
||||
Nested{map[interface{}]*Nested{"a": {1, 2}}, 2},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Nested{map[interface{}]Nested{"a": {1, 2}, "b": {3, 4}}, 2},
|
||||
Nested{map[interface{}]Nested{"a": {1, 5}, "b": {3, 7}}, 2},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Nested{map[interface{}]Nested{"a": {1, 2}, "b": {3, 4}}, 2},
|
||||
Nested{map[interface{}]Nested{"a": {2, 2}, "b": {3, 4}}, 2},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
@ -195,6 +259,169 @@ func TestObjectsExportedFieldsAreEqual(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCopyExportedFields(t *testing.T) {
|
||||
intValue := 1
|
||||
|
||||
cases := []struct {
|
||||
input interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{
|
||||
input: Nested{"a", "b"},
|
||||
expected: Nested{"a", nil},
|
||||
},
|
||||
{
|
||||
input: Nested{&intValue, 2},
|
||||
expected: Nested{&intValue, nil},
|
||||
},
|
||||
{
|
||||
input: Nested{nil, 3},
|
||||
expected: Nested{nil, nil},
|
||||
},
|
||||
{
|
||||
input: S{1, Nested{2, 3}, 4, Nested{5, 6}},
|
||||
expected: S{1, Nested{2, nil}, nil, Nested{}},
|
||||
},
|
||||
{
|
||||
input: S3{},
|
||||
expected: S3{},
|
||||
},
|
||||
{
|
||||
input: S3{&Nested{1, 2}, &Nested{3, 4}},
|
||||
expected: S3{&Nested{1, nil}, &Nested{3, nil}},
|
||||
},
|
||||
{
|
||||
input: S3{Exported1: &Nested{"a", "b"}},
|
||||
expected: S3{Exported1: &Nested{"a", nil}},
|
||||
},
|
||||
{
|
||||
input: S4{[]*Nested{
|
||||
nil,
|
||||
{1, 2},
|
||||
}},
|
||||
expected: S4{[]*Nested{
|
||||
nil,
|
||||
{1, nil},
|
||||
}},
|
||||
},
|
||||
{
|
||||
input: S4{[]*Nested{
|
||||
{1, 2}},
|
||||
},
|
||||
expected: S4{[]*Nested{
|
||||
{1, nil}},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: S4{[]*Nested{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
}},
|
||||
expected: S4{[]*Nested{
|
||||
{1, nil},
|
||||
{3, nil},
|
||||
}},
|
||||
},
|
||||
{
|
||||
input: S5{Exported: Nested{"a", "b"}},
|
||||
expected: S5{Exported: Nested{"a", nil}},
|
||||
},
|
||||
{
|
||||
input: S6{"a", "b"},
|
||||
expected: S6{"a", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
output := copyExportedFields(c.input)
|
||||
if !ObjectsAreEqualValues(c.expected, output) {
|
||||
t.Errorf("%#v, %#v should be equal", c.expected, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualExportedValues(t *testing.T) {
|
||||
cases := []struct {
|
||||
value1 interface{}
|
||||
value2 interface{}
|
||||
expectedEqual bool
|
||||
expectedFail string
|
||||
}{
|
||||
{
|
||||
value1: S{1, Nested{2, 3}, 4, Nested{5, 6}},
|
||||
value2: S{1, Nested{2, nil}, nil, Nested{}},
|
||||
expectedEqual: true,
|
||||
},
|
||||
{
|
||||
value1: S{1, Nested{2, 3}, 4, Nested{5, 6}},
|
||||
value2: S{1, Nested{1, nil}, nil, Nested{}},
|
||||
expectedEqual: false,
|
||||
expectedFail: `
|
||||
Diff:
|
||||
--- Expected
|
||||
+++ Actual
|
||||
@@ -3,3 +3,3 @@
|
||||
Exported2: (assert.Nested) {
|
||||
- Exported: (int) 2,
|
||||
+ Exported: (int) 1,
|
||||
notExported: (interface {}) <nil>`,
|
||||
},
|
||||
{
|
||||
value1: S3{&Nested{1, 2}, &Nested{3, 4}},
|
||||
value2: S3{&Nested{"a", 2}, &Nested{3, 4}},
|
||||
expectedEqual: false,
|
||||
expectedFail: `
|
||||
Diff:
|
||||
--- Expected
|
||||
+++ Actual
|
||||
@@ -2,3 +2,3 @@
|
||||
Exported1: (*assert.Nested)({
|
||||
- Exported: (int) 1,
|
||||
+ Exported: (string) (len=1) "a",
|
||||
notExported: (interface {}) <nil>`,
|
||||
},
|
||||
{
|
||||
value1: S4{[]*Nested{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
}},
|
||||
value2: S4{[]*Nested{
|
||||
{1, "a"},
|
||||
{2, "b"},
|
||||
}},
|
||||
expectedEqual: false,
|
||||
expectedFail: `
|
||||
Diff:
|
||||
--- Expected
|
||||
+++ Actual
|
||||
@@ -7,3 +7,3 @@
|
||||
(*assert.Nested)({
|
||||
- Exported: (int) 3,
|
||||
+ Exported: (int) 2,
|
||||
notExported: (interface {}) <nil>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
mockT := new(mockTestingT)
|
||||
|
||||
actual := EqualExportedValues(mockT, c.value1, c.value2)
|
||||
if actual != c.expectedEqual {
|
||||
t.Errorf("Expected EqualExportedValues to be %t, but was %t", c.expectedEqual, actual)
|
||||
}
|
||||
|
||||
actualFail := mockT.errorString()
|
||||
if !strings.Contains(actualFail, c.expectedFail) {
|
||||
t.Errorf("Contains failure should include %q but was %q", c.expectedFail, actualFail)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestImplements(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system.
|
||||
//
|
||||
// Example Usage
|
||||
// # Example Usage
|
||||
//
|
||||
// The following is a complete example using assert in a standard test function:
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
|
@ -33,7 +34,7 @@
|
|||
// assert.Equal(a, b, "The two words should be the same.")
|
||||
// }
|
||||
//
|
||||
// Assertions
|
||||
// # Assertions
|
||||
//
|
||||
// Assertions allow you to easily write test code, and are global funcs in the `assert` package.
|
||||
// All assertion functions take, as the first argument, the `*testing.T` object provided by the
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/stretchr/testify
|
||||
|
||||
go 1.13
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Package mock provides a system by which it is possible to mock your objects
|
||||
// and verify calls are happening as expected.
|
||||
//
|
||||
// Example Usage
|
||||
// # Example Usage
|
||||
//
|
||||
// The mock package provides an object, Mock, that tracks activity on another object. It is usually
|
||||
// embedded into a test object as shown below:
|
||||
|
|
|
@ -199,12 +199,14 @@ func (c *Call) Maybe() *Call {
|
|||
// Mock.
|
||||
// On("MyMethod", 1).Return(nil).
|
||||
// On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error"))
|
||||
//
|
||||
//go:noinline
|
||||
func (c *Call) On(methodName string, arguments ...interface{}) *Call {
|
||||
return c.Parent.On(methodName, arguments...)
|
||||
}
|
||||
|
||||
// Unset removes a mock handler from being called.
|
||||
//
|
||||
// test.On("func", mock.Anything).Unset()
|
||||
func (c *Call) Unset() *Call {
|
||||
var unlockOnce sync.Once
|
||||
|
@ -764,6 +766,7 @@ type AnythingOfTypeArgument string
|
|||
// name of the type to check for. Used in Diff and Assert.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// Assert(t, AnythingOfType("string"), AnythingOfType("int"))
|
||||
func AnythingOfType(t string) AnythingOfTypeArgument {
|
||||
return AnythingOfTypeArgument(t)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// Package require implements the same assertions as the `assert` package but
|
||||
// stops test execution when a test fails.
|
||||
//
|
||||
// Example Usage
|
||||
// # Example Usage
|
||||
//
|
||||
// The following is a complete example using require in a standard test function:
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
// "github.com/stretchr/testify/require"
|
||||
|
@ -18,7 +19,7 @@
|
|||
//
|
||||
// }
|
||||
//
|
||||
// Assertions
|
||||
// # Assertions
|
||||
//
|
||||
// The `require` package have same global functions as in the `assert` package,
|
||||
// but instead of returning a boolean result they call `t.FailNow()`.
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
// Suite object has assertion methods.
|
||||
//
|
||||
// A crude example:
|
||||
//
|
||||
// // Basic imports
|
||||
// import (
|
||||
// "testing"
|
||||
|
|
Loading…
Reference in New Issue