go.mod
go.sum
prequire.go
prequire_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
modulegithub.com/brandur/prequire | |
go1.19 | |
requiregithub.com/stretchr/testifyv1.8.1 | |
require ( | |
github.com/davecgh/go-spewv1.1.1// indirect | |
github.com/pmezard/go-difflibv1.0.0// indirect | |
gopkg.in/yaml.v3v3.0.1// indirect | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
github.com/davecgh/go-spewv1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
github.com/davecgh/go-spewv1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |
github.com/davecgh/go-spewv1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
github.com/pmezard/go-difflibv1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |
github.com/pmezard/go-difflibv1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |
github.com/stretchr/objxv0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
github.com/stretchr/objxv0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | |
github.com/stretchr/objxv0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | |
github.com/stretchr/testifyv1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
github.com/stretchr/testifyv1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | |
github.com/stretchr/testifyv1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | |
github.com/stretchr/testifyv1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | |
gopkg.in/check.v1v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | |
gopkg.in/check.v1v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
gopkg.in/yaml.v3v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
gopkg.in/yaml.v3v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
gopkg.in/yaml.v3v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package prequire | |
import ( | |
"reflect" | |
"github.com/stretchr/testify/require" | |
) | |
// PartialEqual takes a partially-initialized struct its expected (first | |
// argument) side, and compares all non-zero values on it or any substructs to | |
// the symmetrical values of actual (second argument). In other words, it can | |
// take a struct that only has values that the caller cares about, and compares | |
// just those, even if actual has other non-zero values. | |
// | |
// Example use: | |
// | |
// prequire.PartialEqual(t, dbsqlc.Cluster{ | |
// Environment: sql.NullString{String: string(dbsqlc.ClusterEnvironmentProduction), Valid: true}, | |
// Name: cluster.Name, | |
// }, cluster) | |
// | |
// WARNING: In Go, there's no difference between an explicit zero value versus an | |
// implicit one from when a field is just left out of a struct initialization, | |
// so there's no way for this helper to compare zero values set on the expected | |
// side -- they'll just be silently ignored. So watch out for use of anything | |
// like `false`, `nil`, `sql.NullString{}`, etc. as none of them will work. | |
// Recommended work around is to compare non-zero values with this assertion, | |
// and then assert on values expected to be zero with `require.Zero`. For API | |
// resources, also consider making fields pointer primitives like `*bool` or | |
// `*int` so that an explicit `ptrutil.Ptr(false)` or `ptrutil.Ptr(0)` can be | |
// checked. | |
funcPartialEqual(tTestingT, expected, actualany) { | |
t.Helper() | |
expectedVal:=reflect.ValueOf(expected) | |
actualVal:=reflect.ValueOf(actual) | |
varwasPtrbool | |
ifexpectedVal.Kind() ==reflect.Ptr { | |
ifactualVal.Kind() !=reflect.Ptr { | |
panic("expected value is a pointer; actual value should also be a pointer") | |
} | |
expectedVal=expectedVal.Elem() | |
actualVal=actualVal.Elem() | |
wasPtr=true | |
} | |
switch { | |
caseexpectedVal.Kind() !=reflect.Struct: | |
panic("expected value must be a struct") | |
case!wasPtr&&actualVal.Kind() ==reflect.Ptr: | |
panic("expected value was not a pointer; actual value should also not be a pointer") | |
caseexpectedVal.Type() !=actualVal.Type(): | |
panic("expected and actual values must be the same type") | |
} | |
partialActual:=buildPartialStruct(expectedVal, actualVal) | |
if!wasPtr { | |
partialActual=reflect.ValueOf(partialActual).Elem().Interface() | |
} | |
require.Equal(t, expected, partialActual, "Expected all non-zero fields on structs to be the same") | |
} | |
// Builds a partial struct, taking values from actualVal according to which | |
// values are non-zero in expectedVal. | |
funcbuildPartialStruct(expectedVal, actualVal reflect.Value) any { | |
// Creates a pointer to a new value of the given type. | |
partialActual:=reflect.New(actualVal.Type()) | |
fori:=0; i<expectedVal.NumField(); i++ { | |
expectedField:=expectedVal.Field(i) | |
actualField:=actualVal.Field(i) | |
ifexpectedField.IsZero() { | |
continue | |
} | |
field:=partialActual.Elem().Field(i) | |
switch { | |
caseexpectedField.Kind() ==reflect.Struct: | |
if!actualField.IsZero() { | |
s:=buildPartialStruct(expectedField, actualField) | |
field.Set(reflect.ValueOf(s).Elem()) | |
} | |
caseexpectedField.Kind() ==reflect.Ptr&&expectedField.Elem().Kind() ==reflect.Struct: | |
if!actualField.IsNil() { | |
s:=buildPartialStruct(expectedField.Elem(), actualField.Elem()) | |
field.Set(reflect.ValueOf(s)) | |
} | |
default: | |
field.Set(actualField) | |
} | |
} | |
returnpartialActual.Interface() | |
} | |
// MockT mocks TestingT (or testing.T). | |
// | |
// Copied from testify/require. | |
typeMockTstruct { | |
Failedbool | |
} | |
func (t*MockT) Errorf(formatstring, args...interface{}) { | |
_, _=format, args | |
} | |
func (t*MockT) FailNow() { | |
t.Failed=true | |
} | |
func (t*MockT) Helper() {} | |
// TestingT is an interface wrapper around *testing.T | |
// | |
// Copied from testify/require. Included so we can test failure conditions but | |
// if this becomes too much of a maintenance hassle, it's probably fine to just | |
// remove it. | |
typeTestingTinterface { | |
Errorf(formatstring, args...interface{}) | |
FailNow() | |
Helper() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package prequire_test | |
import ( | |
"testing" | |
"github.com/brandur/prequire" | |
"github.com/stretchr/testify/require" | |
) | |
funcTestPartialEqual(t*testing.T) { | |
typetestStructstruct { | |
Boolbool | |
Intint | |
Pointer*string | |
Stringstring | |
} | |
target:=&testStruct{ | |
Bool: false, | |
Int: 123, | |
Pointer: ptr("hello"), | |
String: "hello", | |
} | |
typetestContainerstruct { | |
Intint | |
Pointer*testStruct | |
ValuetestStruct | |
} | |
targetContainer:=&testContainer{ | |
Int: 123, | |
Pointer: target, | |
Value: *target, | |
} | |
// Here to show a hidden danger of using this assertion -- if a zero value | |
// is accidentally used in the expected struct (left side) then it's not | |
// considered for comparison, even if it was intended to be. Moreover, we | |
// can't detect the problem because even with reflection there's no way to | |
// differentiate a zero value that was set explicitly versus one that | |
// occurred implicitly by leaving it out. | |
// | |
// This will be dangerous for things like boolean `false`, but also `nil` | |
// for pointers and `0` for int, and something to watch out for. | |
t.Run("AccidentalNoOp", func(t*testing.T) { | |
prequire.PartialEqual(t, | |
&testStruct{Bool: false}, | |
&testStruct{Bool: true}, | |
) | |
}) | |
t.Run("SingleField", func(t*testing.T) { | |
// Bool | |
prequire.PartialEqual(t, &testStruct{Bool: false}, target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testStruct{Bool: true}, target) | |
}) | |
// Int | |
prequire.PartialEqual(t, &testStruct{Int: 123}, target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testStruct{Int: 122}, target) | |
}) | |
// Pointer | |
prequire.PartialEqual(t, &testStruct{Pointer: ptr("hello")}, target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testStruct{Pointer: ptr("goodbye")}, target) | |
}) | |
// String | |
prequire.PartialEqual(t, &testStruct{String: "hello"}, target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testStruct{String: "goodbye"}, target) | |
}) | |
}) | |
t.Run("NonPointerStruct", func(t*testing.T) { | |
prequire.PartialEqual(t, testStruct{String: "hello"}, *target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, testStruct{String: "goodbye"}, *target) | |
}) | |
}) | |
t.Run("MultipleFields", func(t*testing.T) { | |
prequire.PartialEqual(t, &testStruct{Int: 123, String: "hello"}, target) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testStruct{Int: 123, String: "goodbye"}, target) | |
}) | |
}) | |
t.Run("Substruct", func(t*testing.T) { | |
// Differing inner field | |
prequire.PartialEqual(t, &testContainer{Int: 123, Pointer: &testStruct{String: "hello"}}, targetContainer) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testContainer{Int: 123, Pointer: &testStruct{String: "goodbye"}}, targetContainer) | |
}) | |
// Differing outer field | |
prequire.PartialEqual(t, &testContainer{Int: 123, Pointer: &testStruct{String: "hello"}}, targetContainer) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testContainer{Int: 124, Pointer: &testStruct{String: "hello"}}, targetContainer) | |
}) | |
// Struct value instead of pointer | |
prequire.PartialEqual(t, &testContainer{Int: 123, Value: testStruct{String: "hello"}}, targetContainer) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testContainer{Int: 123, Value: testStruct{String: "goodbye"}}, targetContainer) | |
}) | |
// Nil and zero substructs | |
prequire.PartialEqual(t, &testContainer{Int: 123}, targetContainer) | |
expectFailure(t, func(t prequire.TestingT) { | |
prequire.PartialEqual(t, &testContainer{Int: 123, Pointer: &testStruct{String: "goodbye"}}, &testContainer{Int: 123}) | |
}) | |
}) | |
t.Run("MisusePanics", func(t*testing.T) { | |
require.PanicsWithValue(t, "expected value is a pointer; actual value should also be a pointer", func() { | |
prequire.PartialEqual(t, | |
&testStruct{String: "hello"}, | |
testStruct{String: "hello"}, | |
) | |
}) | |
require.PanicsWithValue(t, "expected value must be a struct", func() { | |
prequire.PartialEqual(t, "hello", "hello") | |
}) | |
require.PanicsWithValue(t, "expected value was not a pointer; actual value should also not be a pointer", func() { | |
prequire.PartialEqual(t, | |
testStruct{String: "hello"}, | |
&testStruct{String: "hello"}, | |
) | |
}) | |
require.PanicsWithValue(t, "expected and actual values must be the same type", func() { | |
prequire.PartialEqual(t, | |
&testStruct{Int: 123}, | |
&testContainer{Int: 123}, | |
) | |
}) | |
}) | |
} | |
funcexpectFailure(t*testing.T, ffunc(t prequire.TestingT)) { | |
t.Helper() | |
mockT:=&prequire.MockT{} | |
f(mockT) | |
require.True(t, mockT.Failed, "Expected MockT to have failed, but it didn't") | |
} | |
// ptr returns a pointer to the given value. | |
funcptr[Tany](vT) *T { | |
return&v | |
} |