From f26cbc7db2a51748aa3106c718256a881d063f8c Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Tue, 9 Jan 2024 11:05:35 +0000 Subject: [PATCH 1/3] Add tests for `nullable` type We have spun out a separate package, `oapi-codegen/nullable` as a step towards deepmap/oapi-codegen#1039. Until we have implemented #27, we cannot add an explicit type alias in this package, so we can at least add some tests to cover additional functionality and expectations that the package should have when interplaying with `oapi-codegen`. Co-authored-by: Sebastien Guilloux Co-authored-by: Ashutosh Kumar --- go.mod | 1 + go.sum | 2 + types/nullable_test.go | 444 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 447 insertions(+) create mode 100644 types/nullable_test.go diff --git a/go.mod b/go.mod index 8bcdd587..209b3f81 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/uuid v1.5.0 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.11.4 + github.com/oapi-codegen/nullable v1.0.0 github.com/stretchr/testify v1.8.4 ) diff --git a/go.sum b/go.sum index fa19a22a..b3711a89 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oapi-codegen/nullable v1.0.0 h1:FjV9h/GLxYxc3wSSFUafVMvi0lhrpvUcCbEkE04y0fw= +github.com/oapi-codegen/nullable v1.0.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/types/nullable_test.go b/types/nullable_test.go new file mode 100644 index 00000000..90d969a9 --- /dev/null +++ b/types/nullable_test.go @@ -0,0 +1,444 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/oapi-codegen/nullable" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type SimpleStringNullableRequired struct { + // A required field must be `nullable.Nullable` + Name nullable.Nullable[string] `json:"name"` +} + +func TestSimpleStringNullableRequired(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + wantNull: false, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + wantNull: false, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + wantNull: true, + }, + + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleStringNullableRequired + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + }) + } +} + +type SimpleStringNullableOptional struct { + // An optional field must be `nullable.Nullable` and have the JSON tag `omitempty` + Name nullable.Nullable[string] `json:"name,omitempty"` +} + +func TestSimpleStringNullableOptional(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantSpecified bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + wantNull: true, + wantSpecified: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleStringNullableOptional + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.Name.IsSpecified(), "IsSpecified") + }) + } +} + +type SimpleIntNullableRequired struct { + // Nullable type should be used for `nullable and required` fields. + ReplicaCount nullable.Nullable[int] `json:"replica_count"` +} + +func TestSimpleIntNullableRequired(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replica_count":1}`), + wantNull: false, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replica_count":0}`), + wantNull: false, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replica_count":null}`), + wantNull: true, + }, + + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableRequired + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + }) + } +} + +type SimpleIntNullableOptional struct { + ReplicaCount nullable.Nullable[int] `json:"replica_count,omitempty"` +} + +func TestSimpleIntNullableOptional(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantSpecified bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replica_count":1}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replica_count":0}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replica_count":null}`), + wantNull: true, + wantSpecified: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableOptional + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.ReplicaCount.IsSpecified(), "IsSpecified") + }) + } +} + +func TestNullableOptional_MarshalJSON(t *testing.T) { + type testCase struct { + name string + obj SimpleIntNullableOptional + want []byte + } + tests := []testCase{ + { + // When object is not set, and it is an optional type, it does not marshal + name: "when obj is explicitly not set", + want: []byte(`{}`), + }, + { + name: "when obj is explicitly set to a specific value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullableWithValue(5), + }, + want: []byte(`{"replica_count":5}`), + }, + { + name: "when obj is explicitly set to zero value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullableWithValue(0), + }, + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to null value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullNullable[int](), + }, + want: []byte(`{"replica_count":null}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.obj) + require.NoError(t, err) + assert.Equalf(t, tt.want, got, "MarshalJSON()") + }) + } +} + +func TestNullableOptional_UnmarshalJSON(t *testing.T) { + type testCase struct { + name string + json []byte + assert func(t *testing.T, obj SimpleIntNullableOptional) + } + tests := []testCase{ + { + name: "when not provided", + json: []byte(`{}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be specified") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + }, + }, + + { + name: "when explicitly set to zero value", + json: []byte(`{"replica_count":0}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be specified") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 0, actual, "replica count value should be 0") + }, + }, + + { + name: "when explicitly set to null value", + json: []byte(`{"replica_count":null}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + }, + }, + + { + name: "when explicitly set to a specific value", + json: []byte(`{"replica_count":5}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be null") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 5, actual, "replica count value should be 5") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableOptional + err := json.Unmarshal(tt.json, &obj) + require.NoError(t, err) + tt.assert(t, obj) + }) + } +} + +func TestNullableRequired_MarshalJSON(t *testing.T) { + type testCase struct { + name string + obj SimpleIntNullableRequired + want []byte + } + tests := []testCase{ + { + // When object is not set ( not provided in json ) + // it marshals to zero value. + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "when obj is explicitly not set", + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to a specific value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullableWithValue(5), + }, + want: []byte(`{"replica_count":5}`), + }, + { + name: "when obj is explicitly set to zero value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullableWithValue(0), + }, + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to null value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullNullable[int](), + }, + want: []byte(`{"replica_count":null}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.obj) + require.NoError(t, err) + assert.Equalf(t, tt.want, got, "MarshalJSON()") + }) + } +} + +func TestNullableRequired_UnmarshalJSON(t *testing.T) { + type testCase struct { + name string + json []byte + assert func(t *testing.T, obj SimpleIntNullableRequired) + } + tests := []testCase{ + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "when not provided", + json: []byte(`{}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + }, + }, + + { + name: "when explicitly set to zero value", + json: []byte(`{"replica_count":0}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 0, actual, "replica count value should be 0") + }, + }, + + { + name: "when explicitly set to null value", + json: []byte(`{"replica_count":null}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + }, + }, + + { + name: "when explicitly set to a specific value", + json: []byte(`{"replica_count":5}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 5, actual, "replica count value should be 5") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableRequired + err := json.Unmarshal(tt.json, &obj) + require.NoError(t, err) + + tt.assert(t, obj) + }) + } +} From 09846bcfb37f6f25730e7c6840fc6e22ee959f39 Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Tue, 9 Jan 2024 17:21:39 +0530 Subject: [PATCH 2/3] refactor unit tests Signed-off-by: Ashutosh Kumar --- types/nullable_test.go | 246 +++++++++++++++++++++++++++-------------- 1 file changed, 164 insertions(+), 82 deletions(-) diff --git a/types/nullable_test.go b/types/nullable_test.go index 90d969a9..7fafd085 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -2,50 +2,55 @@ package types import ( "encoding/json" - "testing" - + "fmt" "github.com/oapi-codegen/nullable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" ) type SimpleStringNullableRequired struct { - // A required field must be `nullable.Nullable` + // A required field must be `nullable.Nullable`. Name nullable.Nullable[string] `json:"name"` } func TestSimpleStringNullableRequired(t *testing.T) { type testCase struct { - name string - jsonInput []byte - wantNull bool + name string + jsonInput []byte + wantNull bool + wantSpecified bool } tests := []testCase{ { - name: "simple object: set name to some non null value", - jsonInput: []byte(`{"name":"yolo"}`), - wantNull: false, + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + wantNull: false, + wantSpecified: true, }, { - name: "simple object: set name to empty string value", - jsonInput: []byte(`{"name":""}`), - wantNull: false, + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + wantNull: false, + wantSpecified: true, }, { - name: "simple object: set name to null value", - jsonInput: []byte(`{"name":null}`), - wantNull: true, + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + wantNull: true, + wantSpecified: true, }, { // For Nullable and required field there will not be a case when it is not // provided in json payload but this test case exists just to explain // the behaviour - name: "simple object: do not provide name in json data", - jsonInput: []byte(`{}`), - wantNull: false, + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, }, } for _, tt := range tests { @@ -54,12 +59,13 @@ func TestSimpleStringNullableRequired(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) require.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.Name.IsSpecified(), "IsSpecified()") }) } } type SimpleStringNullableOptional struct { - // An optional field must be `nullable.Nullable` and have the JSON tag `omitempty` + // An optional field must be `nullable.Nullable` and have the JSON tag `omitempty`. Name nullable.Nullable[string] `json:"name,omitempty"` } @@ -105,48 +111,53 @@ func TestSimpleStringNullableOptional(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) require.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") - assert.Equalf(t, tt.wantSpecified, obj.Name.IsSpecified(), "IsSpecified") + assert.Equalf(t, tt.wantSpecified, obj.Name.IsSpecified(), "IsSpecified()") }) } } type SimpleIntNullableRequired struct { - // Nullable type should be used for `nullable and required` fields. + // A required field must be `nullable.Nullable`. ReplicaCount nullable.Nullable[int] `json:"replica_count"` } func TestSimpleIntNullableRequired(t *testing.T) { type testCase struct { - name string - jsonInput []byte - wantNull bool + name string + jsonInput []byte + wantNull bool + wantSpecified bool } tests := []testCase{ { - name: "simple object: set name to some non null value", - jsonInput: []byte(`{"replica_count":1}`), - wantNull: false, + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replica_count":1}`), + wantNull: false, + wantSpecified: true, }, { - name: "simple object: set name to empty value", - jsonInput: []byte(`{"replica_count":0}`), - wantNull: false, + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replica_count":0}`), + wantNull: false, + wantSpecified: true, }, { - name: "simple object: set name to null value", - jsonInput: []byte(`{"replica_count":null}`), - wantNull: true, + name: "simple object: set name to null value", + jsonInput: []byte(`{"replica_count":null}`), + wantNull: true, + wantSpecified: true, }, { // For Nullable and required field there will not be a case when it is not // provided in json payload but this test case exists just to explain // the behaviour - name: "simple object: do not provide name in json data", - jsonInput: []byte(`{}`), - wantNull: false, + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, }, } for _, tt := range tests { @@ -155,11 +166,13 @@ func TestSimpleIntNullableRequired(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) require.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.ReplicaCount.IsSpecified(), "IsSpecified()") }) } } type SimpleIntNullableOptional struct { + // An optional field must be `nullable.Nullable` and have the JSON tag `omitempty` ReplicaCount nullable.Nullable[int] `json:"replica_count,omitempty"` } @@ -206,7 +219,7 @@ func TestSimpleIntNullableOptional(t *testing.T) { require.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") - assert.Equalf(t, tt.wantSpecified, obj.ReplicaCount.IsSpecified(), "IsSpecified") + assert.Equalf(t, tt.wantSpecified, obj.ReplicaCount.IsSpecified(), "IsSpecified()") }) } } @@ -219,28 +232,35 @@ func TestNullableOptional_MarshalJSON(t *testing.T) { } tests := []testCase{ { - // When object is not set, and it is an optional type, it does not marshal + // When object is not set ( not provided in json ) + // it marshals to zero value. name: "when obj is explicitly not set", want: []byte(`{}`), }, { name: "when obj is explicitly set to a specific value", obj: SimpleIntNullableOptional{ - ReplicaCount: nullable.NewNullableWithValue(5), + ReplicaCount: nullable.Nullable[int]{ + true: 5, + }, }, want: []byte(`{"replica_count":5}`), }, { name: "when obj is explicitly set to zero value", obj: SimpleIntNullableOptional{ - ReplicaCount: nullable.NewNullableWithValue(0), + ReplicaCount: nullable.Nullable[int]{ + true: 0, + }, }, want: []byte(`{"replica_count":0}`), }, { name: "when obj is explicitly set to null value", obj: SimpleIntNullableOptional{ - ReplicaCount: nullable.NewNullNullable[int](), + ReplicaCount: nullable.Nullable[int]{ + false: 0, + }, }, want: []byte(`{"replica_count":null}`), }, @@ -248,8 +268,9 @@ func TestNullableOptional_MarshalJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := json.Marshal(tt.obj) + fmt.Println(string(got)) require.NoError(t, err) - assert.Equalf(t, tt.want, got, "MarshalJSON()") + assert.Equalf(t, string(tt.want), string(got), "MarshalJSON()") }) } } @@ -258,60 +279,59 @@ func TestNullableOptional_UnmarshalJSON(t *testing.T) { type testCase struct { name string json []byte - assert func(t *testing.T, obj SimpleIntNullableOptional) + assert func(obj SimpleIntNullableOptional, t *testing.T) } tests := []testCase{ { name: "when not provided", json: []byte(`{}`), - assert: func(t *testing.T, obj SimpleIntNullableOptional) { + assert: func(obj SimpleIntNullableOptional, t *testing.T) { t.Helper() - assert.Falsef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be specified") - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsSpecified(), "replica count should not be set") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") }, }, { name: "when explicitly set to zero value", json: []byte(`{"replica_count":0}`), - assert: func(t *testing.T, obj SimpleIntNullableOptional) { + assert: func(obj SimpleIntNullableOptional, t *testing.T) { t.Helper() - assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be specified") - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") - actual, err := obj.ReplicaCount.Get() + val, err := obj.ReplicaCount.Get() require.NoError(t, err) + assert.Equalf(t, 0, val, "replica count value should be 0") - assert.Equalf(t, 0, actual, "replica count value should be 0") }, }, { name: "when explicitly set to null value", json: []byte(`{"replica_count":null}`), - assert: func(t *testing.T, obj SimpleIntNullableOptional) { + assert: func(obj SimpleIntNullableOptional, t *testing.T) { t.Helper() - assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") - assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Equalf(t, true, obj.ReplicaCount.IsNull(), "replica count should be null") }, }, { name: "when explicitly set to a specific value", json: []byte(`{"replica_count":5}`), - assert: func(t *testing.T, obj SimpleIntNullableOptional) { + assert: func(obj SimpleIntNullableOptional, t *testing.T) { t.Helper() - assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be null") - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") - actual, err := obj.ReplicaCount.Get() + val, err := obj.ReplicaCount.Get() require.NoError(t, err) - - assert.Equalf(t, 5, actual, "replica count value should be 5") + assert.Equalf(t, 5, val, "replica count value should be 5") }, }, } @@ -320,7 +340,7 @@ func TestNullableOptional_UnmarshalJSON(t *testing.T) { var obj SimpleIntNullableOptional err := json.Unmarshal(tt.json, &obj) require.NoError(t, err) - tt.assert(t, obj) + tt.assert(obj, t) }) } } @@ -344,21 +364,27 @@ func TestNullableRequired_MarshalJSON(t *testing.T) { { name: "when obj is explicitly set to a specific value", obj: SimpleIntNullableRequired{ - ReplicaCount: nullable.NewNullableWithValue(5), + ReplicaCount: nullable.Nullable[int]{ + true: 5, + }, }, want: []byte(`{"replica_count":5}`), }, { name: "when obj is explicitly set to zero value", obj: SimpleIntNullableRequired{ - ReplicaCount: nullable.NewNullableWithValue(0), + ReplicaCount: nullable.Nullable[int]{ + true: 0, + }, }, want: []byte(`{"replica_count":0}`), }, { name: "when obj is explicitly set to null value", obj: SimpleIntNullableRequired{ - ReplicaCount: nullable.NewNullNullable[int](), + ReplicaCount: nullable.Nullable[int]{ + false: 0, + }, }, want: []byte(`{"replica_count":null}`), }, @@ -366,8 +392,9 @@ func TestNullableRequired_MarshalJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := json.Marshal(tt.obj) + fmt.Println(string(got)) require.NoError(t, err) - assert.Equalf(t, tt.want, got, "MarshalJSON()") + assert.Equalf(t, string(tt.want), string(got), "MarshalJSON()") }) } } @@ -376,7 +403,7 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { type testCase struct { name string json []byte - assert func(t *testing.T, obj SimpleIntNullableRequired) + assert func(obj SimpleIntNullableRequired, t *testing.T) } tests := []testCase{ { @@ -385,50 +412,52 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { // the behaviour name: "when not provided", json: []byte(`{}`), - assert: func(t *testing.T, obj SimpleIntNullableRequired) { + assert: func(obj SimpleIntNullableRequired, t *testing.T) { t.Helper() - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsSpecified(), "replica count should not be set") }, }, { name: "when explicitly set to zero value", json: []byte(`{"replica_count":0}`), - assert: func(t *testing.T, obj SimpleIntNullableRequired) { + assert: func(obj SimpleIntNullableRequired, t *testing.T) { t.Helper() - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") - actual, err := obj.ReplicaCount.Get() + val, err := obj.ReplicaCount.Get() require.NoError(t, err) - - assert.Equalf(t, 0, actual, "replica count value should be 0") + assert.Equalf(t, 0, val, "replica count value should be 0") }, }, { name: "when explicitly set to null value", json: []byte(`{"replica_count":null}`), - assert: func(t *testing.T, obj SimpleIntNullableRequired) { + assert: func(obj SimpleIntNullableRequired, t *testing.T) { t.Helper() - assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Equalf(t, true, obj.ReplicaCount.IsNull(), "replica count should be null") }, }, { name: "when explicitly set to a specific value", json: []byte(`{"replica_count":5}`), - assert: func(t *testing.T, obj SimpleIntNullableRequired) { + assert: func(obj SimpleIntNullableRequired, t *testing.T) { t.Helper() - assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") - actual, err := obj.ReplicaCount.Get() + val, err := obj.ReplicaCount.Get() require.NoError(t, err) - - assert.Equalf(t, 5, actual, "replica count value should be 5") + assert.Equalf(t, 5, val, "replica count value should be 5") }, }, } @@ -437,8 +466,61 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { var obj SimpleIntNullableRequired err := json.Unmarshal(tt.json, &obj) require.NoError(t, err) - - tt.assert(t, obj) + tt.assert(obj, t) }) } } + +// Idempotency tests for nullable and optional +type StringNullableOptional struct { + ID nullable.Nullable[string] `json:"id,omitempty"` + Name nullable.Nullable[string] `json:"name,omitempty"` + Address nullable.Nullable[string] `json:"address,omitempty"` +} + +func TestNullableOptionalUnmarshalIdempotency(t *testing.T) { + var obj1 StringNullableOptional + originalJSON1 := []byte(`{}`) + err := json.Unmarshal(originalJSON1, &obj1) + require.NoError(t, err) + newJSON1, err := json.Marshal(obj1) + require.Equal(t, originalJSON1, newJSON1) + + var obj2 StringNullableOptional + originalJSON2 := []byte(`{"id":"12esd412"}`) + err2 := json.Unmarshal(originalJSON2, &obj2) + require.NoError(t, err2) + newJSON2, err2 := json.Marshal(obj2) + require.Equal(t, originalJSON2, newJSON2) +} + +func TestNullableOptionalMarshalIdempotency(t *testing.T) { + obj1 := StringNullableOptional{ + ID: nullable.Nullable[string]{ + true: "id-1", + }, + Name: nullable.Nullable[string]{ + true: "", + }, + Address: nullable.Nullable[string]{ + false: "", + }, + } + expectedJSON1 := []byte(`{"id":"id-1","name":"","address":null}`) + gotJSON1, err1 := json.Marshal(obj1) + require.NoError(t, err1) + require.Equal(t, expectedJSON1, gotJSON1) + + obj2 := StringNullableOptional{ + ID: nullable.Nullable[string]{ + true: "id-1", + }, + Address: nullable.Nullable[string]{ + false: "", + }, + } + expectedJSON2 := []byte(`{"id":"id-1","address":null}`) + gotJSON2, err2 := json.Marshal(obj2) + require.NoError(t, err2) + require.Equal(t, expectedJSON2, gotJSON2) +} From e6e916a63ce757a8e4376eda53add1b39c829a23 Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Tue, 9 Jan 2024 17:51:47 +0530 Subject: [PATCH 3/3] add unit test for complex struct Signed-off-by: Ashutosh Kumar --- types/nullable_test.go | 225 +++++++++++++++++++++++++++++++++-------- 1 file changed, 185 insertions(+), 40 deletions(-) diff --git a/types/nullable_test.go b/types/nullable_test.go index 7fafd085..207250f6 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -3,10 +3,11 @@ package types import ( "encoding/json" "fmt" + "testing" + "github.com/oapi-codegen/nullable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) type SimpleStringNullableRequired struct { @@ -279,55 +280,54 @@ func TestNullableOptional_UnmarshalJSON(t *testing.T) { type testCase struct { name string json []byte - assert func(obj SimpleIntNullableOptional, t *testing.T) + assert func(t *testing.T, obj SimpleIntNullableOptional) } tests := []testCase{ { name: "when not provided", json: []byte(`{}`), - assert: func(obj SimpleIntNullableOptional, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableOptional) { t.Helper() - assert.Equalf(t, false, obj.ReplicaCount.IsSpecified(), "replica count should not be set") - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Falsef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") }, }, { name: "when explicitly set to zero value", json: []byte(`{"replica_count":0}`), - assert: func(obj SimpleIntNullableOptional, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableOptional) { t.Helper() - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") val, err := obj.ReplicaCount.Get() require.NoError(t, err) assert.Equalf(t, 0, val, "replica count value should be 0") - }, }, { name: "when explicitly set to null value", json: []byte(`{"replica_count":null}`), - assert: func(obj SimpleIntNullableOptional, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableOptional) { t.Helper() - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") - assert.Equalf(t, true, obj.ReplicaCount.IsNull(), "replica count should be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") }, }, { name: "when explicitly set to a specific value", json: []byte(`{"replica_count":5}`), - assert: func(obj SimpleIntNullableOptional, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableOptional) { t.Helper() - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should not be null") - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") val, err := obj.ReplicaCount.Get() require.NoError(t, err) @@ -340,7 +340,8 @@ func TestNullableOptional_UnmarshalJSON(t *testing.T) { var obj SimpleIntNullableOptional err := json.Unmarshal(tt.json, &obj) require.NoError(t, err) - tt.assert(obj, t) + + tt.assert(t, obj) }) } } @@ -403,7 +404,7 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { type testCase struct { name string json []byte - assert func(obj SimpleIntNullableRequired, t *testing.T) + assert func(t *testing.T, obj SimpleIntNullableRequired) } tests := []testCase{ { @@ -412,22 +413,22 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { // the behaviour name: "when not provided", json: []byte(`{}`), - assert: func(obj SimpleIntNullableRequired, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableRequired) { t.Helper() - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") - assert.Equalf(t, false, obj.ReplicaCount.IsSpecified(), "replica count should not be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Falsef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be set") }, }, { name: "when explicitly set to zero value", json: []byte(`{"replica_count":0}`), - assert: func(obj SimpleIntNullableRequired, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableRequired) { t.Helper() - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") val, err := obj.ReplicaCount.Get() require.NoError(t, err) @@ -438,22 +439,22 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { { name: "when explicitly set to null value", json: []byte(`{"replica_count":null}`), - assert: func(obj SimpleIntNullableRequired, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableRequired) { t.Helper() - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") - assert.Equalf(t, true, obj.ReplicaCount.IsNull(), "replica count should be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") }, }, { name: "when explicitly set to a specific value", json: []byte(`{"replica_count":5}`), - assert: func(obj SimpleIntNullableRequired, t *testing.T) { + assert: func(t *testing.T, obj SimpleIntNullableRequired) { t.Helper() - assert.Equalf(t, false, obj.ReplicaCount.IsNull(), "replica count should not be null") - assert.Equalf(t, true, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") val, err := obj.ReplicaCount.Get() require.NoError(t, err) @@ -466,35 +467,179 @@ func TestNullableRequired_UnmarshalJSON(t *testing.T) { var obj SimpleIntNullableRequired err := json.Unmarshal(tt.json, &obj) require.NoError(t, err) - tt.assert(obj, t) + + tt.assert(t, obj) + }) + } +} + +type ComplexNullable struct { + Config nullable.Nullable[Config] `json:"config,omitempty"` + Location nullable.Nullable[string] `json:"location"` + NodeCount nullable.Nullable[int] `json:"node_count,omitempty"` +} + +type Config struct { + CPU nullable.Nullable[string] `json:"cpu,omitempty"` + RAM nullable.Nullable[string] `json:"ram,omitempty"` +} + +func TestComplexNullable(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + assert func(t *testing.T, obj ComplexNullable) + } + tests := []testCase{ + { + name: "complex object: empty value", + jsonInput: []byte(`{}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Falsef(t, obj.Config.IsSpecified(), "config should not be set") + assert.Falsef(t, obj.Config.IsNull(), "config should not be null") + + assert.Falsef(t, obj.NodeCount.IsSpecified(), "node count should not be set") + assert.Falsef(t, obj.NodeCount.IsNull(), "node count should not be null") + + assert.Falsef(t, obj.Location.IsSpecified(), "location should not be set") + assert.Falsef(t, obj.Location.IsNull(), "location should not be null") + }, + }, + { + name: "complex object: empty config value", + jsonInput: []byte(`{"config":{}}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Truef(t, obj.Config.IsSpecified(), "config should be set") + assert.Falsef(t, obj.Config.IsNull(), "config should not be null") + + gotConfig, err := obj.Config.Get() + require.NoError(t, err) + + assert.Falsef(t, gotConfig.CPU.IsSpecified(), "cpu should not be set") + assert.Falsef(t, gotConfig.CPU.IsNull(), "cpu should not be null") + + assert.Falsef(t, gotConfig.RAM.IsSpecified(), "ram should not be set") + assert.Falsef(t, gotConfig.RAM.IsNull(), "ram should not be null") + }, + }, + + { + name: "complex object: setting only cpu config value", + jsonInput: []byte(`{"config":{"cpu":"500"}}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Truef(t, obj.Config.IsSpecified(), "config should be set") + assert.Falsef(t, obj.Config.IsNull(), "config should not be null") + + gotConfig, err := obj.Config.Get() + require.NoError(t, err) + + assert.Truef(t, gotConfig.CPU.IsSpecified(), "cpu should be set") + assert.Falsef(t, gotConfig.CPU.IsNull(), "cpu should not be null") + + assert.Falsef(t, gotConfig.RAM.IsSpecified(), "ram should not be set") + assert.Falsef(t, gotConfig.RAM.IsNull(), "ram should not be null") + }, + }, + + { + name: "complex object: setting only ram config value", + jsonInput: []byte(`{"config":{"ram":"1024"}}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Truef(t, obj.Config.IsSpecified(), "config should be set") + assert.Falsef(t, obj.Config.IsNull(), "config should not be null") + + gotConfig, err := obj.Config.Get() + require.NoError(t, err) + + assert.Falsef(t, gotConfig.CPU.IsSpecified(), "cpu should not be set") + assert.Falsef(t, gotConfig.CPU.IsNull(), "cpu should not be null") + + assert.Truef(t, gotConfig.RAM.IsSpecified(), "ram should be set") + assert.Falsef(t, gotConfig.RAM.IsNull(), "ram should not be null") + }, + }, + + { + name: "complex object: setting config to null", + jsonInput: []byte(`{"config":null}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Truef(t, obj.Config.IsSpecified(), "config should be set") + assert.Truef(t, obj.Config.IsNull(), "config should not be null") + }, + }, + + { + name: "complex object: setting only cpu config to null", + jsonInput: []byte(`{"config":{"cpu":null}}`), + assert: func(t *testing.T, obj ComplexNullable) { + t.Helper() + + assert.Truef(t, obj.Config.IsSpecified(), "config should be set") + assert.Falsef(t, obj.Config.IsNull(), "config should not be null") + + gotConfig, err := obj.Config.Get() + require.NoError(t, err) + + assert.Truef(t, gotConfig.CPU.IsSpecified(), "cpu should be set") + assert.Truef(t, gotConfig.CPU.IsNull(), "cpu should be null") + + assert.Falsef(t, gotConfig.RAM.IsSpecified(), "ram should not be set") + assert.Falsef(t, gotConfig.RAM.IsNull(), "ram should not be null") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj ComplexNullable + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + + tt.assert(t, obj) }) } } -// Idempotency tests for nullable and optional +// Tests to validate that repeated unmarshal and marshal should not change +// the JSON for optional and nullable. type StringNullableOptional struct { ID nullable.Nullable[string] `json:"id,omitempty"` Name nullable.Nullable[string] `json:"name,omitempty"` Address nullable.Nullable[string] `json:"address,omitempty"` } -func TestNullableOptionalUnmarshalIdempotency(t *testing.T) { +func TestNullableOptionalUnmarshal(t *testing.T) { var obj1 StringNullableOptional originalJSON1 := []byte(`{}`) + err := json.Unmarshal(originalJSON1, &obj1) require.NoError(t, err) newJSON1, err := json.Marshal(obj1) + require.NoError(t, err) + require.Equal(t, originalJSON1, newJSON1) var obj2 StringNullableOptional originalJSON2 := []byte(`{"id":"12esd412"}`) - err2 := json.Unmarshal(originalJSON2, &obj2) - require.NoError(t, err2) - newJSON2, err2 := json.Marshal(obj2) + + err = json.Unmarshal(originalJSON2, &obj2) + require.NoError(t, err) + newJSON2, err := json.Marshal(obj2) + require.NoError(t, err) + require.Equal(t, originalJSON2, newJSON2) } -func TestNullableOptionalMarshalIdempotency(t *testing.T) { +func TestNullableOptionalMarshal(t *testing.T) { obj1 := StringNullableOptional{ ID: nullable.Nullable[string]{ true: "id-1", @@ -507,8 +652,8 @@ func TestNullableOptionalMarshalIdempotency(t *testing.T) { }, } expectedJSON1 := []byte(`{"id":"id-1","name":"","address":null}`) - gotJSON1, err1 := json.Marshal(obj1) - require.NoError(t, err1) + gotJSON1, err := json.Marshal(obj1) + require.NoError(t, err) require.Equal(t, expectedJSON1, gotJSON1) obj2 := StringNullableOptional{ @@ -520,7 +665,7 @@ func TestNullableOptionalMarshalIdempotency(t *testing.T) { }, } expectedJSON2 := []byte(`{"id":"id-1","address":null}`) - gotJSON2, err2 := json.Marshal(obj2) - require.NoError(t, err2) + gotJSON2, err := json.Marshal(obj2) + require.NoError(t, err) require.Equal(t, expectedJSON2, gotJSON2) }