diff --git a/.gitignore b/.gitignore index eb81a9e..57a47c8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,5 @@ _testmain.go # Vim swap files *.swp -# Don't apply gitignore rules to vendored projects -!/vendor/** +# Vendored files +/vendor diff --git a/README.md b/README.md index d61c02b..c4e4c60 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://github.com/nicheinc/nullable/actions/workflows/ci.yml/badge.svg)](https://github.com/nicheinc/nullable/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/nicheinc/nullable)](https://goreportcard.com/report/github.com/nicheinc/nullable) -[![Godoc](https://godoc.org/github.com/nicheinc/nullable?status.svg)](https://godoc.org/github.com/nicheinc/nullable) +[![Godoc](https://godoc.org/github.com/nicheinc/nullable?status.svg)](https://godoc.org/github.com/nicheinc/nullable) [![license](https://img.shields.io/github/license/nicheinc/nullable.svg?cacheSeconds=2592000)](LICENSE) This package provides types representing updates to struct fields, @@ -20,15 +20,24 @@ indicate deletion. If a certain key is not present, the corresponding field is not modified. We want to define go structs corresponding to these updates, which need to be marshalled to/from JSON. -If we were to use pointer fields with the `omitempty` JSON struct tag option for +If we were to use pointer fields with the `omitzero` JSON struct tag option for these structs, then fields explicitly set to `nil` to be removed would instead simply be absent from the marshalled JSON, i.e. unchanged. If we were to use -pointer fields without `omitempty`, then `nil` fields would be present and -`null` in the JSON output, i.e. removed. +pointer fields without `omitzero`, then `nil` fields would be present and `null` +in the JSON output, i.e. removed. -The `Update` and `SliceUpdate` types distinguish between no-op and removal -updates, allowing them to correctly and seamlessly unmarshal themselves from -JSON. +The `nup.Update` and `nup.SliceUpdate` types distinguish between no-op and +removal updates, allowing them to correctly and seamlessly unmarshal themselves +from JSON. + +## Marshalling + +For best results, use +[`json.Marshal`](https://pkg.go.dev/encoding/json#Marshal)'s `omitzero` struct +tag option on all struct fields of type `nup.Update` or `nup.SliceUpdate`. This +will ensure that if the field is a no-op, it's correctly omitted from the JSON +output. (If the `omitzero` tag is absent, the field will be marshalled as +`null`.) ## Installation diff --git a/go.mod b/go.mod index b13067c..f3a211c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/nicheinc/nullable/v2 -go 1.18 +go 1.24 + +require github.com/nicheinc/expect v0.2.0 + +require ( + github.com/google/go-cmp v0.5.9 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect +) diff --git a/go.sum b/go.sum index e69de29..2e91079 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/nicheinc/expect v0.2.0 h1:Z0xKpZiDQsRuxhm2HsUh4M9datV1QgM/DpCFWvN/rpY= +github.com/nicheinc/expect v0.2.0/go.mod h1:NRiUkkvrrIz1Uj0VccPt3ZBZcqGU3RnPSK55oYVSJiY= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= diff --git a/nup/doc.go b/nup/doc.go index c732638..b9d19fe 100644 --- a/nup/doc.go +++ b/nup/doc.go @@ -3,59 +3,31 @@ Package nup (think "nullable update") provides types representing updates to struct fields, distinguishing between no-ops, removals, and modifications when marshalling those updates to/from JSON. -Motivation +# Motivation It's often useful to define data updates using JSON objects, where each -key-value pair represents an update to a field, using null to indicate deletion. -If a certain key is not present, the corresponding field is not modified. We -want to define go structs corresponding to these updates, which need to be -marshalled to/from JSON. +key-value pair represents an update to a field, using a value of null to +indicate deletion. If a certain key is not present, the corresponding field is +not modified. We want to define go structs corresponding to these updates, which +need to be marshalled to/from JSON. -If we were to use pointer fields with the omitempty JSON struct tag option for +If we were to use pointer fields with the omitzero JSON struct tag option for these structs, then fields explicitly set to nil to be removed would instead simply be absent from the marshalled JSON, i.e. unchanged. If we were to use -pointer fields without omitempty, then nil fields would be present and null in +pointer fields without omitzero, then nil fields would be present and null in the JSON output, i.e. removed. -The Update and SliceUpdate types distinguish between no-op and removal updates, -allowing them to correctly and seamlessly unmarshal themselves from JSON. +The nup.Update and nup.SliceUpdate types distinguish between no-op and removal +updates, allowing them to correctly and seamlessly unmarshal themselves from +JSON. -Marshalling +# Marshalling -Unfortunately, the default JSON marshaller is unaware of nup types, and -providing a MarshalJSON implementation in the types themselves is insufficient -because it's the containing struct that determines which field names appear in -the JSON output. +For best results, use [json.Marshal]'s omitzero struct tag option on all struct +fields of type nup.Update or nup.SliceUpdate. This will ensure that if the field +is a no-op, it's correctly omitted from the JSON output. (If the omitzero tag is +absent, the field will be marshalled as null.) -A custom implementation can use an ad-hoc struct mirroring the original struct -(but with an extra level of indirection), along with a check per field that the -field is set before copying it into the output struct, but implementing this -method for every update type is laborious and error-prone. To avoid this -boilerplate, this package provides the nup.MarshalJSON function, which -implements a version of json.Marshal that respects the no-op/remove distinction. - -Besides its treatment of Update/SliceUpdate fields, nup.MarshalJSON behaves -exactly like json.Marshal (https://golang.org/pkg/encoding/json/#Marshal), with -the following exceptions: - -• Anonymous fields are skipped - -• The string tag option is ignored - -Note that the omitempty option does not affect nup types. The default JSON -marshaller never omits struct values, but nup.MarshalJSON takes the use of a nup -update type per se as an indication to omit the field if it's a no-op, even if -omitempty is absent. - -To avoid accidentally calling the default implementation, it's prudent to -implement for each relevant type a MarshalJSON method that simply calls -nup.MarshalJSON. - -There are several outstanding golang proposals that could eliminate the need for -a custom MarshalJSON implementation in the future. One proposal -(https://github.com/golang/go/issues/11939) would allow zero-valued structs to -be treated as empty with respect to omitempty fields. Another proposal -(https://github.com/golang/go/issues/50480) would allow types to return (nil, -nil) from MarshalJSON to indicate they should be treated as empty by omitempty. +[json.Marshal]: https://pkg.go.dev/encoding/json#Marshal */ package nup diff --git a/nup/marshalJSON.go b/nup/marshalJSON.go index 31adfb5..d1f84d6 100644 --- a/nup/marshalJSON.go +++ b/nup/marshalJSON.go @@ -6,6 +6,10 @@ import ( "strings" ) +// Deprecated: As of Go 1.24, json.Marshal handles fields of nup types correctly +// by default, as long as those fields are marked with the "omitzero" JSON +// struct tag. +// // MarshalJSON is a reimplementation of json.Marshal that understands nup types. // Any struct that contains Update or SliceUpdate fields should call this // function instead of the default json.Marshal. For more info, see @@ -142,13 +146,16 @@ func getKeyName(field reflect.StructField, fieldValue reflect.Value) *string { // modification, are permitted provided that the following conditions are // met: // -// * Redistributions of source code must retain the above copyright +// - Redistributions of source code must retain the above copyright +// // notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above +// - Redistributions in binary form must reproduce the above +// // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. -// * Neither the name of Google Inc. nor the names of its +// - Neither the name of Google Inc. nor the names of its +// // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // diff --git a/nup/marshalJSON_test.go b/nup/marshalJSON_test.go index eddd7c5..b40d440 100644 --- a/nup/marshalJSON_test.go +++ b/nup/marshalJSON_test.go @@ -3,8 +3,9 @@ package nup import ( "encoding/json" "errors" - "reflect" "testing" + + "github.com/nicheinc/expect" ) func TestMarshalJSON_OneWay(t *testing.T) { @@ -209,13 +210,9 @@ func TestMarshalJSON_OneWay(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := MarshalJSON(testCase.input) - if err != nil { - t.Errorf("Error while marshalling: %v", err) - } + expect.ErrorNil(t, err) actual := string(data) - if actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } @@ -305,19 +302,14 @@ func roundtrip[T any](t *testing.T, testName string, input T) { t.Run(testName, func(t *testing.T) { t.Helper() // Marshal input. - data, err := MarshalJSON(input) - if err != nil { - t.Errorf("Error while marshalling: %v", err) - } + data, marshalErr := MarshalJSON(input) + expect.ErrorNil(t, marshalErr) // Unmarshal resulting JSON to output. var output T - if err := json.Unmarshal(data, &output); err != nil { - t.Errorf("Error while unmarshalling: %v", err) - } + unmarshalErr := json.Unmarshal(data, &output) + expect.ErrorNil(t, unmarshalErr) // Marshalling then unmarshalling should result in the same value. - if !reflect.DeepEqual(input, output) { - t.Errorf("Expected: %v. Actual: %v", input, output) - } + expect.Equal(t, input, output) }) } @@ -355,12 +347,8 @@ func TestMarshalJSON_FieldErrors(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := MarshalJSON(testCase.input) - if data != nil { - t.Errorf("Expected nil data. Actual: %v", string(data)) - } - if err == nil { - t.Error("Expected a non-nil error") - } + expect.ErrorNonNil(t, err) + expect.Equal(t, data, nil) }) } } diff --git a/nup/operation_test.go b/nup/operation_test.go index ae15a4d..b946a1f 100644 --- a/nup/operation_test.go +++ b/nup/operation_test.go @@ -1,6 +1,10 @@ package nup -import "testing" +import ( + "testing" + + "github.com/nicheinc/expect" +) func TestOperation_String(t *testing.T) { testCases := []struct { @@ -28,9 +32,7 @@ func TestOperation_String(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := testCase.op.String() - if actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } diff --git a/nup/sliceUpdate.go b/nup/sliceUpdate.go index 087e5f6..ebe4c4f 100644 --- a/nup/sliceUpdate.go +++ b/nup/sliceUpdate.go @@ -62,6 +62,11 @@ func (u SliceUpdate[T]) IsNoop() bool { return u.op == OpNoop } +// IsZero is equivalent to IsNoop. +func (u SliceUpdate[T]) IsZero() bool { + return u.IsNoop() +} + // IsRemove returns whether this update is a remove operation. IsRemove is // equivalent to Operation() == OpRemove. func (u SliceUpdate[T]) IsRemove() bool { @@ -118,6 +123,14 @@ func (u SliceUpdate[T]) Diff(value []T) SliceUpdate[T] { return u } +// MarshalJSON implements json.Marshaler. +func (u SliceUpdate[T]) MarshalJSON() ([]byte, error) { + if u.op == OpSet { + return json.Marshal(u.value) + } + return []byte("null"), nil +} + // UnmarshalJSON implements json.Unmarshaler. func (u *SliceUpdate[T]) UnmarshalJSON(data []byte) error { if string(data) == "null" { diff --git a/nup/sliceUpdate_test.go b/nup/sliceUpdate_test.go index 0447add..f572474 100644 --- a/nup/sliceUpdate_test.go +++ b/nup/sliceUpdate_test.go @@ -3,8 +3,9 @@ package nup import ( "encoding/json" "fmt" - "reflect" "testing" + + "github.com/nicheinc/expect" ) var ( @@ -15,6 +16,35 @@ var ( // Ensure implementation of the updateMarshaller interface. var _ updateMarshaller = &SliceUpdate[int]{} +func TestSliceUpdate_MarshalJSON(t *testing.T) { + type testCase struct { + update SliceUpdate[int] + expected string + } + run := func(name string, testCase testCase) { + t.Helper() + t.Run(name, func(t *testing.T) { + t.Helper() + actual, err := json.Marshal(testCase.update) + expect.ErrorNil(t, err) + expect.Equal(t, string(actual), testCase.expected) + }) + } + + run("Noop", testCase{ + update: SliceNoop[int](), + expected: "null", + }) + run("Remove", testCase{ + update: SliceRemove[int](), + expected: "null", + }) + run("Set", testCase{ + update: SliceRemoveOrSet[int](testSlice1), + expected: "[1]", + }) +} + func TestSliceUpdate_UnmarshalJSON(t *testing.T) { testCases := []struct { name string @@ -48,12 +78,9 @@ func TestSliceUpdate_UnmarshalJSON(t *testing.T) { var dst struct { Update SliceUpdate[int] `json:"update"` } - if err := json.Unmarshal([]byte(testCase.json), &dst); err != nil { - t.Errorf("Error unmarshaling JSON: %s", err) - } - if !dst.Update.Equal(testCase.expected) { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, dst.Update) - } + err := json.Unmarshal([]byte(testCase.json), &dst) + expect.ErrorNil(t, err) + expect.Equal(t, dst.Update, testCase.expected) }) } } @@ -84,9 +111,7 @@ func TestSliceRemoveOrSet(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := SliceRemoveOrSet(testCase.value) - if !actual.Equal(testCase.expected) { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } @@ -121,12 +146,8 @@ func TestSliceUpdate_ValueOperation(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actualValue, actualOp := testCase.update.ValueOperation() - if !reflect.DeepEqual(actualValue, testCase.expectedValue) { - t.Errorf("Expected value: %v. Actual: %v", testCase.expectedValue, actualValue) - } - if actualOp != testCase.expectedOp { - t.Errorf("Expected operation: %v. Actual: %v", testCase.expectedOp, actualOp) - } + expect.Equal(t, actualValue, testCase.expectedValue) + expect.Equal(t, actualOp, testCase.expectedOp) }) } } @@ -137,6 +158,7 @@ func TestSliceUpdate_OperationAccessors(t *testing.T) { update SliceUpdate[int] expectedOp Operation expectedIsNoop bool + expectedIsZero bool expectedIsRemove bool expectedIsSet bool expectedIsChange bool @@ -146,6 +168,7 @@ func TestSliceUpdate_OperationAccessors(t *testing.T) { update: SliceNoop[int](), expectedOp: OpNoop, expectedIsNoop: true, + expectedIsZero: true, }, { name: "Remove", @@ -165,28 +188,12 @@ func TestSliceUpdate_OperationAccessors(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - var ( - op = testCase.update.Operation() - isNoop = testCase.update.IsNoop() - isRemove = testCase.update.IsRemove() - isSet = testCase.update.IsSet() - isChange = testCase.update.IsChange() - ) - if op != testCase.expectedOp { - t.Errorf("Expected Operation(): %v. Actual: %v", testCase.expectedOp, op) - } - if isNoop != testCase.expectedIsNoop { - t.Errorf("Expected IsNoop(): %v. Actual: %v", testCase.expectedIsNoop, isNoop) - } - if isRemove != testCase.expectedIsRemove { - t.Errorf("Expected IsRemove(): %v. Actual: %v", testCase.expectedIsRemove, isRemove) - } - if isSet != testCase.expectedIsSet { - t.Errorf("Expected IsSet(): %v. Actual: %v", testCase.expectedIsSet, isSet) - } - if isChange != testCase.expectedIsChange { - t.Errorf("Expected IsChange(): %v. Actual: %v", testCase.expectedIsChange, isChange) - } + expect.Equal(t, testCase.update.Operation(), testCase.expectedOp) + expect.Equal(t, testCase.update.IsNoop(), testCase.expectedIsNoop) + expect.Equal(t, testCase.update.IsZero(), testCase.expectedIsZero) + expect.Equal(t, testCase.update.IsRemove(), testCase.expectedIsRemove) + expect.Equal(t, testCase.update.IsSet(), testCase.expectedIsSet) + expect.Equal(t, testCase.update.IsChange(), testCase.expectedIsChange) }) } } @@ -221,12 +228,8 @@ func TestSliceUpdate_Value(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { value, isSet := testCase.update.Value() - if !reflect.DeepEqual(value, testCase.expectedValue) { - t.Errorf("Expected value: %v. Actual: %v", testCase.expectedValue, value) - } - if isSet != testCase.expectedIsSet { - t.Errorf("Expected isSet: %v. Actual: %v", testCase.expectedIsSet, isSet) - } + expect.Equal(t, value, testCase.expectedValue) + expect.Equal(t, isSet, testCase.expectedIsSet) }) } } @@ -257,9 +260,7 @@ func TestSliceUpdate_ValueOrNil(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := testCase.update.ValueOrNil() - if !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Expected value: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } @@ -289,9 +290,8 @@ func TestSliceUpdate_Apply(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.u.Apply(testSlice1); !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.u.Apply(testSlice1) + expect.Equal(t, actual, testCase.expected) }) } } @@ -337,9 +337,8 @@ func TestSliceUpdate_Diff(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.u.Diff(testCase.value); !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.u.Diff(testCase.value) + expect.Equal(t, actual, testCase.expected) }) } } @@ -379,9 +378,8 @@ func TestSliceUpdate_IsSetTo(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.u.IsSetTo(testSlice1); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.u.IsSetTo(testSlice1) + expect.Equal(t, actual, testCase.expected) }) } } @@ -419,9 +417,8 @@ func TestSliceUpdate_IsSetSuchThat(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.u.IsSetSuchThat(isCouple); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.u.IsSetSuchThat(isCouple) + expect.Equal(t, actual, testCase.expected) }) } } @@ -466,9 +463,8 @@ func TestSliceUpdate_String(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.u.String(); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.u.String() + expect.Equal(t, actual, testCase.expected) }) } } @@ -521,9 +517,7 @@ func TestSliceUpdate_Equal(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := testCase.first.Equal(testCase.second) - if actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } diff --git a/nup/update.go b/nup/update.go index 9c1c6fa..cf823b5 100644 --- a/nup/update.go +++ b/nup/update.go @@ -65,6 +65,11 @@ func (u Update[T]) IsNoop() bool { return u.op == OpNoop } +// IsZero is equivalent to IsNoop. +func (u Update[T]) IsZero() bool { + return u.IsNoop() +} + // IsRemove returns whether this update is a remove operation. IsRemove is // equivalent to Operation() == OpRemove. func (u Update[T]) IsRemove() bool { @@ -155,7 +160,14 @@ func (u Update[T]) DiffPtr(value *T) Update[T] { default: return u.Diff(*value) } +} +// MarshalJSON implements json.Marshaler. +func (u Update[T]) MarshalJSON() ([]byte, error) { + if u.op == OpSet { + return json.Marshal(u.value) + } + return []byte("null"), nil } // UnmarshalJSON implements json.Unmarshaler. diff --git a/nup/update_test.go b/nup/update_test.go index e2482ed..c578a4b 100644 --- a/nup/update_test.go +++ b/nup/update_test.go @@ -3,8 +3,9 @@ package nup import ( "encoding/json" "fmt" - "reflect" "testing" + + "github.com/nicheinc/expect" ) var testValue = 42 @@ -12,6 +13,35 @@ var testValue = 42 // Ensure implementation of the updateMarshaller interface. var _ updateMarshaller = &Update[int]{} +func TestUpdate_MarshalJSON(t *testing.T) { + type testCase struct { + update Update[int] + expected string + } + run := func(name string, testCase testCase) { + t.Helper() + t.Run(name, func(t *testing.T) { + t.Helper() + actual, err := json.Marshal(testCase.update) + expect.ErrorNil(t, err) + expect.Equal(t, string(actual), testCase.expected) + }) + } + + run("Noop", testCase{ + update: Noop[int](), + expected: "null", + }) + run("Remove", testCase{ + update: Remove[int](), + expected: "null", + }) + run("Set", testCase{ + update: Set[int](testValue), + expected: "42", + }) +} + func TestUpdate_UnmarshalJSON(t *testing.T) { testCases := []struct { name string @@ -40,12 +70,9 @@ func TestUpdate_UnmarshalJSON(t *testing.T) { var dst struct { Update Update[int] `json:"update"` } - if err := json.Unmarshal([]byte(testCase.json), &dst); err != nil { - t.Errorf("Error unmarshaling JSON: %s", err) - } - if dst.Update != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, dst.Update) - } + err := json.Unmarshal([]byte(testCase.json), &dst) + expect.ErrorNil(t, err) + expect.Equal(t, dst.Update, testCase.expected) }) } } @@ -80,9 +107,7 @@ func TestRemoveOrSet(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := RemoveOrSet(testCase.ptr) - if actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } @@ -117,12 +142,8 @@ func TestUpdate_ValueOperation(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actualValue, actualOp := testCase.update.ValueOperation() - if actualValue != testCase.expectedValue { - t.Errorf("Expected value: %v. Actual: %v", testCase.expectedValue, actualValue) - } - if actualOp != testCase.expectedOp { - t.Errorf("Expected operation: %v. Actual: %v", testCase.expectedOp, actualOp) - } + expect.Equal(t, actualValue, testCase.expectedValue) + expect.Equal(t, actualOp, testCase.expectedOp) }) } } @@ -133,6 +154,7 @@ func TestUpdate_OperationAccessors(t *testing.T) { update Update[int] expectedOp Operation expectedIsNoop bool + expectedIsZero bool expectedIsRemove bool expectedIsSet bool expectedIsChange bool @@ -142,6 +164,7 @@ func TestUpdate_OperationAccessors(t *testing.T) { update: Noop[int](), expectedOp: OpNoop, expectedIsNoop: true, + expectedIsZero: true, }, { name: "Remove", @@ -161,28 +184,12 @@ func TestUpdate_OperationAccessors(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - var ( - op = testCase.update.Operation() - isNoop = testCase.update.IsNoop() - isRemove = testCase.update.IsRemove() - isSet = testCase.update.IsSet() - isChange = testCase.update.IsChange() - ) - if op != testCase.expectedOp { - t.Errorf("Expected Operation(): %v. Actual: %v", testCase.expectedOp, op) - } - if isNoop != testCase.expectedIsNoop { - t.Errorf("Expected IsNoop(): %v. Actual: %v", testCase.expectedIsNoop, isNoop) - } - if isRemove != testCase.expectedIsRemove { - t.Errorf("Expected IsRemove(): %v. Actual: %v", testCase.expectedIsRemove, isRemove) - } - if isSet != testCase.expectedIsSet { - t.Errorf("Expected IsSet(): %v. Actual: %v", testCase.expectedIsSet, isSet) - } - if isChange != testCase.expectedIsChange { - t.Errorf("Expected IsChange(): %v. Actual: %v", testCase.expectedIsChange, isChange) - } + expect.Equal(t, testCase.update.Operation(), testCase.expectedOp) + expect.Equal(t, testCase.update.IsNoop(), testCase.expectedIsNoop) + expect.Equal(t, testCase.update.IsZero(), testCase.expectedIsZero) + expect.Equal(t, testCase.update.IsRemove(), testCase.expectedIsRemove) + expect.Equal(t, testCase.update.IsSet(), testCase.expectedIsSet) + expect.Equal(t, testCase.update.IsChange(), testCase.expectedIsChange) }) } } @@ -217,12 +224,8 @@ func TestUpdate_Value(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { value, isSet := testCase.update.Value() - if value != testCase.expectedValue { - t.Errorf("Expected value: %v. Actual: %v", testCase.expectedValue, value) - } - if isSet != testCase.expectedOK { - t.Errorf("Expected isSet: %v. Actual: %v", testCase.expectedOK, isSet) - } + expect.Equal(t, value, testCase.expectedValue) + expect.Equal(t, isSet, testCase.expectedOK) }) } } @@ -253,9 +256,7 @@ func TestUpdate_ValueOrNil(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := testCase.update.ValueOrNil() - if !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Expected value: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } } @@ -286,9 +287,8 @@ func TestUpdate_Apply(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.Apply(value); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.Apply(value) + expect.Equal(t, actual, testCase.expected) }) } } @@ -322,9 +322,8 @@ func TestUpdate_ApplyPtr(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.ApplyPtr(&value1); !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.ApplyPtr(&value1) + expect.Equal(t, actual, testCase.expected) }) } } @@ -374,9 +373,8 @@ func TestUpdate_Diff(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.Diff(testCase.value); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.Diff(testCase.value) + expect.Equal(t, actual, testCase.expected) }) } } @@ -433,9 +431,8 @@ func TestUpdate_DiffPtr(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.DiffPtr(testCase.value); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.DiffPtr(testCase.value) + expect.Equal(t, actual, testCase.expected) }) } } @@ -471,9 +468,8 @@ func TestUpdate_IsSetTo(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.IsSetTo(value); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.IsSetTo(value) + expect.Equal(t, actual, testCase.expected) }) } } @@ -511,9 +507,8 @@ func TestUpdate_IsSetSuchThat(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.IsSetSuchThat(isNegative); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.IsSetSuchThat(isNegative) + expect.Equal(t, actual, testCase.expected) }) } } @@ -553,9 +548,8 @@ func TestUpdate_String(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - if actual := testCase.update.String(); actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + actual := testCase.update.String() + expect.Equal(t, actual, testCase.expected) }) } } @@ -614,9 +608,7 @@ func TestUpdate_Equal(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual := testCase.first.Equal(testCase.second) - if actual != testCase.expected { - t.Errorf("Expected: %v. Actual: %v", testCase.expected, actual) - } + expect.Equal(t, actual, testCase.expected) }) } }