From 3c556cd17b03b087d5d0c2c9e0db0cf97ffdbebc Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Fri, 10 Jan 2020 15:37:02 +0000 Subject: [PATCH 01/14] Remove redundant err check and panic --- core.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/core.go b/core.go index 9666485..8cc903c 100644 --- a/core.go +++ b/core.go @@ -2,7 +2,6 @@ package jsonassert import ( "encoding/json" - "errors" "fmt" "strings" ) @@ -60,12 +59,7 @@ func (a *Asserter) pathassertf(path, act, exp string) { } func serialize(a interface{}) string { - bytes, err := json.Marshal(a) - if err != nil { - // Really don't want to panic here, but I can't see a reasonable solution. - // If this line *does* get executed then we should really investigate what kind of input was given - panic(errors.New("unexpected failure to re-serialize nested JSON. Please raise an issue including this error message and both the expected and actual JSON strings you used to trigger this panic" + err.Error())) - } + bytes, _ := json.Marshal(a) return string(bytes) } From afee279ad24e893e02f3a9c6432d274a6b884d98 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Fri, 10 Jan 2020 15:38:18 +0000 Subject: [PATCH 02/14] Define language features lower bound --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index ea7e5da..7f381c8 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/kinbiko/jsonassert + +go 1.12 From 75946d85c71861ac3dea44e251cc882959e23e17 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Fri, 10 Jan 2020 16:28:26 +0000 Subject: [PATCH 03/14] Initial <> implementation --- array.go | 34 ++++++++++++++++++++++++++++++++-- integration_test.go | 8 ++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/array.go b/array.go index 0e4b353..25b0df0 100644 --- a/array.go +++ b/array.go @@ -8,6 +8,13 @@ import ( func (a *Asserter) checkArray(path string, act, exp []interface{}) { a.tt.Helper() + + var unordered bool + if len(exp) > 0 && exp[0] == "<>" { + unordered = true + exp = exp[1:] + } + if len(act) != len(exp) { a.tt.Errorf("length of arrays at '%s' were different. Expected array to be of length %d, but contained %d element(s)", path, len(exp), len(act)) serializedAct, serializedExp := serialize(act), serialize(exp) @@ -18,11 +25,34 @@ func (a *Asserter) checkArray(path string, act, exp []interface{}) { } return } - for i := range act { - a.pathassertf(path+fmt.Sprintf("[%d]", i), serialize(act[i]), serialize(exp[i])) + + if unordered { + for i := range act { + hasMatch := false + for j := range act { + ap := arrayPrinter{} + New(&ap).pathassertf("", serialize(act[i]), serialize(exp[j])) + hasMatch = hasMatch || len(ap) == 0 + } + if !hasMatch { + serializedAct, serializedExp := serialize(act), serialize(exp) + a.tt.Errorf("elements at '%s' are different, even when ignoring order within the array:\nexpected some ordering of\n%s\nbut got\n%s", path, serializedExp, serializedAct) + } + } + } else { + for i := range act { + a.pathassertf(path+fmt.Sprintf("[%d]", i), serialize(act[i]), serialize(exp[i])) + } } } +type arrayPrinter []string + +func (p *arrayPrinter) Errorf(msg string, args ...interface{}) { + n := append(*p, fmt.Sprintf(msg, args...)) + *p = n +} + func extractArray(s string) ([]interface{}, error) { s = strings.TrimSpace(s) if len(s) == 0 { diff --git a/integration_test.go b/integration_test.go index dc9aabb..2023975 100644 --- a/integration_test.go +++ b/integration_test.go @@ -74,6 +74,14 @@ but expected JSON was: {name: "different non-empty arrays", act: `["hello"]`, exp: `["world"]`, msgs: []string{ `expected string at '$[0]' to be 'world' but was 'hello'`, }}, + {name: "identical non-empty unsorted arrays", act: `["hello", "world"]`, exp: `["<>", "world", "hello"]`, msgs: []string{}}, + {name: "different non-empty unsorted arrays", act: `["hello", "world"]`, exp: `["<>", "世界", "hello"]`, msgs: []string{ + `elements at '$' are different, even when ignoring order within the array: +expected some ordering of +["世界","hello"] +but got +["hello","world"]`, + }}, {name: "different length non-empty arrays", act: `["hello", "world"]`, exp: `["world"]`, msgs: []string{ `length of arrays at '$' were different. Expected array to be of length 1, but contained 2 element(s)`, `actual JSON at '$' was: ["hello","world"], but expected JSON was: ["world"]`, From a18f5b04a93fa0ad8392bc50b0ca043dfdec8695 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Wed, 15 Jan 2020 13:32:30 +0000 Subject: [PATCH 04/14] Naive containsf implementation for arrays --- array.go | 42 ++++++++++++++++++++++++++++++++++++ core.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ exports.go | 5 +++++ integration_test.go | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/array.go b/array.go index 25b0df0..1ca2a19 100644 --- a/array.go +++ b/array.go @@ -46,6 +46,48 @@ func (a *Asserter) checkArray(path string, act, exp []interface{}) { } } +func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) { + a.tt.Helper() + + var unordered bool + if len(exp) > 0 && exp[0] == "<>" { + unordered = true + exp = exp[1:] + } + + if len(act) < len(exp) { + a.tt.Errorf("length of expected array at '%s' was longer (length %d) than the actual array (length %d).", path, len(exp), len(act)) + serializedAct, serializedExp := serialize(act), serialize(exp) + if len(serializedAct+serializedExp) < 50 { + a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON was: %+v", path, serializedAct, serializedExp) + } else { + a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON was:\n%+v", path, serializedAct, serializedExp) + } + return + } + + if unordered { + mismatchedExpPaths := map[string]string{} + for i := range exp { + for j := range act { + ap := arrayPrinter{} + serializedAct, serializedExp := serialize(act[i]), serialize(exp[j]) + New(&ap).pathContainsf("", serializedAct, serializedExp) + if len(ap) > 0 { + mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<>" + } + } + } + for path, serializedExp := range mismatchedExpPaths { + a.tt.Errorf("element at %s in the expected payload was not found anywhere in the actual JSON array:\n%s\nnot found in\n%s", path, serializedExp, serialize(act)) + } + } else { + for i := range exp { + a.pathContainsf(fmt.Sprintf("%s[%d]", path, i), serialize(act[i]), serialize(exp[i])) + } + } +} + type arrayPrinter []string func (p *arrayPrinter) Errorf(msg string, args ...interface{}) { diff --git a/core.go b/core.go index 8cc903c..3e3169b 100644 --- a/core.go +++ b/core.go @@ -58,6 +58,58 @@ func (a *Asserter) pathassertf(path, act, exp string) { } } +func (a *Asserter) pathContainsf(path, act, exp string) { + a.tt.Helper() + if act == exp { + return + } + actType, err := findType(act) + if err != nil { + a.tt.Errorf("'actual' JSON is not valid JSON: " + err.Error()) + return + } + expType, err := findType(exp) + if err != nil { + a.tt.Errorf("'expected' JSON is not valid JSON: " + err.Error()) + return + } + + // If we're only caring about the presence of the key, then don't bother checking any further + if expPresence, _ := extractString(exp); expPresence == "<>" { + if actType == jsonNull { + a.tt.Errorf(`expected the presence of any value at '%s', but was absent`, path) + } + return + } + + if actType != expType { + a.tt.Errorf("actual JSON (%s) and expected JSON (%s) were of different types at '%s'", actType, expType, path) + return + } + switch actType { + case jsonBoolean: + actBool, _ := extractBoolean(act) + expBool, _ := extractBoolean(exp) + a.checkBoolean(path, actBool, expBool) + case jsonNumber: + actNumber, _ := extractNumber(act) + expNumber, _ := extractNumber(exp) + a.checkNumber(path, actNumber, expNumber) + case jsonString: + actString, _ := extractString(act) + expString, _ := extractString(exp) + a.checkString(path, actString, expString) + case jsonObject: + actObject, _ := extractObject(act) + expObject, _ := extractObject(exp) + a.checkContainsObject(path, actObject, expObject) + case jsonArray: + actArray, _ := extractArray(act) + expArray, _ := extractArray(exp) + a.checkContainsArray(path, actArray, expArray) + } +} + func serialize(a interface{}) string { bytes, _ := json.Marshal(a) return string(bytes) diff --git a/exports.go b/exports.go index e11fcb7..127ee5f 100644 --- a/exports.go +++ b/exports.go @@ -104,3 +104,8 @@ func (a *Asserter) Assertf(actualJSON, expectedJSON string, fmtArgs ...interface a.tt.Helper() a.pathassertf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...)) } + +func (a *Asserter) Containsf(actualJSON, expectedJSON string, fmtArgs ...interface{}) { + a.tt.Helper() + a.pathContainsf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...)) +} diff --git a/integration_test.go b/integration_test.go index 2023975..6bd739e 100644 --- a/integration_test.go +++ b/integration_test.go @@ -125,6 +125,44 @@ but got } } +func TestContainsf(t *testing.T) { + tt := []struct { + name string + act string + exp string + msgs []string + }{} + for _, tc := range tt { + t.Run(tc.name, func(st *testing.T) { + tp, ja := setup() + ja.Containsf(tc.act, tc.exp) + if got := len(tp.messages); got != len(tc.msgs) { + st.Errorf("expected %d assertion message(s) but got %d", len(tc.msgs), got) + if len(tc.msgs) > 0 { + st.Errorf("Expected the following messages:") + for _, msg := range tc.msgs { + st.Errorf(" - %s", msg) + } + } + + if len(tp.messages) > 0 { + st.Errorf("Got the following messages:") + for _, msg := range tp.messages { + st.Errorf(" - %s", msg) + } + } + return + } + for i := range tc.msgs { + if exp, got := tc.msgs[i], tp.messages[i]; got != exp { + st.Errorf("expected assertion message:\n'%s'\nbut got\n'%s'", exp, got) + } + } + }) + } + +} + func setup() (*testPrinter, *jsonassert.Asserter) { tp := &testPrinter{} return tp, jsonassert.New(tp) From 7704445975afbb69f45ba5090de14ab2ae7dce49 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Wed, 15 Jan 2020 13:37:59 +0000 Subject: [PATCH 05/14] Naive containsf implementation for objects --- object.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/object.go b/object.go index 388ecd7..36e826b 100644 --- a/object.go +++ b/object.go @@ -24,6 +24,22 @@ func (a *Asserter) checkObject(path string, act, exp map[string]interface{}) { } } +func (a *Asserter) checkContainsObject(path string, act, exp map[string]interface{}) { + a.tt.Helper() + + if len(act) > len(exp) { + a.tt.Errorf("expected %d keys at '%s' but only got %d keys", len(exp), path, len(act)) + } + if unique := difference(exp, act); len(unique) != 0 { + a.tt.Errorf("expected object key(s) %+v missing at '%s'", serialize(unique), path) + } + for key := range exp { + if contains(act, key) { + a.pathassertf(path+"."+key, serialize(act[key]), serialize(exp[key])) + } + } +} + func difference(act, exp map[string]interface{}) []string { unique := []string{} for key := range act { From aa1e823bb3f904ec069d3425c68cac1e1568abca Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Wed, 15 Jan 2020 13:44:15 +0000 Subject: [PATCH 06/14] Naive containsf initial core exp check --- core.go | 7 ++++++- exports.go | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core.go b/core.go index 3e3169b..fe24a28 100644 --- a/core.go +++ b/core.go @@ -73,6 +73,9 @@ func (a *Asserter) pathContainsf(path, act, exp string) { a.tt.Errorf("'expected' JSON is not valid JSON: " + err.Error()) return } + if expType == jsonNull { + return // Don't bother checking the field if it's not in the expected payload + } // If we're only caring about the presence of the key, then don't bother checking any further if expPresence, _ := extractString(exp); expPresence == "<>" { @@ -86,7 +89,7 @@ func (a *Asserter) pathContainsf(path, act, exp string) { a.tt.Errorf("actual JSON (%s) and expected JSON (%s) were of different types at '%s'", actType, expType, path) return } - switch actType { + switch expType { case jsonBoolean: actBool, _ := extractBoolean(act) expBool, _ := extractBoolean(exp) @@ -107,6 +110,8 @@ func (a *Asserter) pathContainsf(path, act, exp string) { actArray, _ := extractArray(act) expArray, _ := extractArray(exp) a.checkContainsArray(path, actArray, expArray) + case jsonNull: + // Intentionally don't check as it wasn't expected in the payload } } diff --git a/exports.go b/exports.go index 127ee5f..3a00378 100644 --- a/exports.go +++ b/exports.go @@ -105,6 +105,8 @@ func (a *Asserter) Assertf(actualJSON, expectedJSON string, fmtArgs ...interface a.pathassertf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...)) } +// TODO: remember to document what happens if you call Containsf with a null +// property as currently it will treat it as the key being missing. func (a *Asserter) Containsf(actualJSON, expectedJSON string, fmtArgs ...interface{}) { a.tt.Helper() a.pathContainsf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...)) From a8164b43cde6defb9b5f8d169ce935c2ba27ef8e Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 18:39:01 +0900 Subject: [PATCH 07/14] Clean up unordered checking logic --- array.go | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/array.go b/array.go index 1ca2a19..e6c7524 100644 --- a/array.go +++ b/array.go @@ -27,21 +27,26 @@ func (a *Asserter) checkArray(path string, act, exp []interface{}) { } if unordered { - for i := range act { - hasMatch := false - for j := range act { - ap := arrayPrinter{} - New(&ap).pathassertf("", serialize(act[i]), serialize(exp[j])) - hasMatch = hasMatch || len(ap) == 0 - } - if !hasMatch { - serializedAct, serializedExp := serialize(act), serialize(exp) - a.tt.Errorf("elements at '%s' are different, even when ignoring order within the array:\nexpected some ordering of\n%s\nbut got\n%s", path, serializedExp, serializedAct) - } + a.checkUnorderedArray(path, act, exp) + return + } + + for i := range act { + a.pathassertf(path+fmt.Sprintf("[%d]", i), serialize(act[i]), serialize(exp[i])) + } +} + +func (a *Asserter) checkUnorderedArray(path string, act, exp []interface{}) { + for i := range act { + hasMatch := false + for j := range act { + ap := arrayPrinter{} + New(&ap).pathassertf("", serialize(act[i]), serialize(exp[j])) + hasMatch = hasMatch || len(ap) == 0 } - } else { - for i := range act { - a.pathassertf(path+fmt.Sprintf("[%d]", i), serialize(act[i]), serialize(exp[i])) + if !hasMatch { + serializedAct, serializedExp := serialize(act), serialize(exp) + a.tt.Errorf("elements at '%s' are different, even when ignoring order within the array:\nexpected some ordering of\n%s\nbut got\n%s", path, serializedExp, serializedAct) } } } From 9e3d6e52170a185ce373c515c3d728da9615496d Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:03:34 +0900 Subject: [PATCH 08/14] Start testing containsf --- array.go | 6 +++--- core.go | 3 --- integration_test.go | 37 ++++++++++++++++++++++++++++++++++++- object.go | 2 +- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/array.go b/array.go index e6c7524..784f863 100644 --- a/array.go +++ b/array.go @@ -61,12 +61,12 @@ func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) { } if len(act) < len(exp) { - a.tt.Errorf("length of expected array at '%s' was longer (length %d) than the actual array (length %d).", path, len(exp), len(act)) + a.tt.Errorf("length of expected array at '%s' was longer (length %d) than the actual array (length %d)", path, len(exp), len(act)) serializedAct, serializedExp := serialize(act), serialize(exp) if len(serializedAct+serializedExp) < 50 { - a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON was: %+v", path, serializedAct, serializedExp) + a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON to contain: %+v", path, serializedAct, serializedExp) } else { - a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON was:\n%+v", path, serializedAct, serializedExp) + a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON to contain:\n%+v", path, serializedAct, serializedExp) } return } diff --git a/core.go b/core.go index fe24a28..f439202 100644 --- a/core.go +++ b/core.go @@ -73,9 +73,6 @@ func (a *Asserter) pathContainsf(path, act, exp string) { a.tt.Errorf("'expected' JSON is not valid JSON: " + err.Error()) return } - if expType == jsonNull { - return // Don't bother checking the field if it's not in the expected payload - } // If we're only caring about the presence of the key, then don't bother checking any further if expPresence, _ := extractString(exp); expPresence == "<>" { diff --git a/integration_test.go b/integration_test.go index 6bd739e..26b4b3a 100644 --- a/integration_test.go +++ b/integration_test.go @@ -131,7 +131,42 @@ func TestContainsf(t *testing.T) { act string exp string msgs []string - }{} + }{ + {name: "actual not valid json", act: `foo`, exp: `"foo"`, msgs: []string{ + `'actual' JSON is not valid JSON: unable to identify JSON type of "foo"`, + }}, + {name: "expected not valid json", act: `"foo"`, exp: `foo`, msgs: []string{ + `'expected' JSON is not valid JSON: unable to identify JSON type of "foo"`, + }}, + {name: "number contains a number", act: `5`, exp: `5`, msgs: nil}, + {name: "number does not contain a different number", act: `5`, exp: `-2`, msgs: []string{ + "expected number at '$' to be '-2.0000000' but was '5.0000000'", + }}, + {name: "string contains a string", act: `"foo"`, exp: `"foo"`, msgs: nil}, + {name: "string does not contain a different string", act: `"foo"`, exp: `"bar"`, msgs: []string{ + "expected string at '$' to be 'bar' but was 'foo'", + }}, + {name: "boolean contains a boolean", act: `true`, exp: `true`, msgs: nil}, + {name: "boolean does not contain a different boolean", act: `true`, exp: `false`, msgs: []string{ + "expected boolean at '$' to be false but was true", + }}, + + {name: "empty array contains empty array", act: `[]`, exp: `[]`, msgs: nil}, + {name: "single-element array contains empty array", act: `["fish"]`, exp: `[]`, msgs: nil}, + {name: "unordered empty array contains empty array", act: `["<>"]`, exp: `[]`, msgs: nil}, + {name: "unordered single-element array contains empty array", act: `["<>", "fish"]`, exp: `[]`, msgs: nil}, + {name: "empty array contains single-element array", act: `[]`, exp: `["fish"]`, msgs: []string{ + "length of expected array at '$' was longer (length 1) than the actual array (length 0)", + `actual JSON at '$' was: [], but expected JSON to contain: ["fish"]`, + }}, + + {name: "expected and actual have different types", act: `{"foo": "bar"}`, exp: `null`, msgs: []string{ + "actual JSON (object) and expected JSON (null) were of different types at '$'", + }}, + {name: "expected any value, but got null", act: `{"foo": null}`, exp: `{"foo": "<>"}`, msgs: []string{ + "expected the presence of any value at '$.foo', but was absent", + }}, + } for _, tc := range tt { t.Run(tc.name, func(st *testing.T) { tp, ja := setup() diff --git a/object.go b/object.go index 36e826b..d9344a3 100644 --- a/object.go +++ b/object.go @@ -35,7 +35,7 @@ func (a *Asserter) checkContainsObject(path string, act, exp map[string]interfac } for key := range exp { if contains(act, key) { - a.pathassertf(path+"."+key, serialize(act[key]), serialize(exp[key])) + a.pathContainsf(path+"."+key, serialize(act[key]), serialize(exp[key])) } } } From 57dc913b2ef5b070aaee70818e40cbe9a425f103 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:16:30 +0900 Subject: [PATCH 09/14] Fix contains-logic bug --- array.go | 11 ++++++++--- integration_test.go | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/array.go b/array.go index 784f863..dac8cb6 100644 --- a/array.go +++ b/array.go @@ -74,14 +74,19 @@ func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) { if unordered { mismatchedExpPaths := map[string]string{} for i := range exp { + found := false + serializedExp := serialize(exp[i]) for j := range act { ap := arrayPrinter{} - serializedAct, serializedExp := serialize(act[i]), serialize(exp[j]) + serializedAct := serialize(act[j]) New(&ap).pathContainsf("", serializedAct, serializedExp) - if len(ap) > 0 { - mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<>" + if len(ap) == 0 { + found = true } } + if !found { + mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<>" + } } for path, serializedExp := range mismatchedExpPaths { a.tt.Errorf("element at %s in the expected payload was not found anywhere in the actual JSON array:\n%s\nnot found in\n%s", path, serializedExp, serialize(act)) diff --git a/integration_test.go b/integration_test.go index 26b4b3a..037526b 100644 --- a/integration_test.go +++ b/integration_test.go @@ -153,12 +153,13 @@ func TestContainsf(t *testing.T) { {name: "empty array contains empty array", act: `[]`, exp: `[]`, msgs: nil}, {name: "single-element array contains empty array", act: `["fish"]`, exp: `[]`, msgs: nil}, - {name: "unordered empty array contains empty array", act: `["<>"]`, exp: `[]`, msgs: nil}, - {name: "unordered single-element array contains empty array", act: `["<>", "fish"]`, exp: `[]`, msgs: nil}, + {name: "unordered empty array contains empty array", act: `[]`, exp: `["<>"]`, msgs: nil}, + {name: "unordered single-element array contains empty array", act: `["fish"]`, exp: `["<>"]`, msgs: nil}, {name: "empty array contains single-element array", act: `[]`, exp: `["fish"]`, msgs: []string{ "length of expected array at '$' was longer (length 1) than the actual array (length 0)", `actual JSON at '$' was: [], but expected JSON to contain: ["fish"]`, }}, + {name: "unordered multi-element array contains subset", act: `["alpha", "beta", "gamma"]`, exp: `["<>", "beta", "alpha"]`, msgs: nil}, {name: "expected and actual have different types", act: `{"foo": "bar"}`, exp: `null`, msgs: []string{ "actual JSON (object) and expected JSON (null) were of different types at '$'", From 24d30a9fb1bf18f806479fbb3b712aa53e15d3ac Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:27:15 +0900 Subject: [PATCH 10/14] Support multi-messages in contains IT --- integration_test.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/integration_test.go b/integration_test.go index 037526b..c9117c5 100644 --- a/integration_test.go +++ b/integration_test.go @@ -160,6 +160,26 @@ func TestContainsf(t *testing.T) { `actual JSON at '$' was: [], but expected JSON to contain: ["fish"]`, }}, {name: "unordered multi-element array contains subset", act: `["alpha", "beta", "gamma"]`, exp: `["<>", "beta", "alpha"]`, msgs: nil}, + {name: "unordered multi-element array does not contain single element", act: `["alpha", "beta", "gamma"]`, exp: `["<>", "delta", "alpha"]`, msgs: []string{ + `element at $[1] in the expected payload was not found anywhere in the actual JSON array: +"delta" +not found in +["alpha","beta","gamma"]`, + }}, + {name: "unordered multi-element array contains none of multi-element array", act: `["alpha", "beta", "gamma"]`, exp: `["<>", "delta", "pi", "omega"]`, msgs: []string{ + `element at $[1] in the expected payload was not found anywhere in the actual JSON array: +"delta" +not found in +["alpha","beta","gamma"]`, + `element at $[2] in the expected payload was not found anywhere in the actual JSON array: +"pi" +not found in +["alpha","beta","gamma"]`, + `element at $[3] in the expected payload was not found anywhere in the actual JSON array: +"omega" +not found in +["alpha","beta","gamma"]`, + }}, {name: "expected and actual have different types", act: `{"foo": "bar"}`, exp: `null`, msgs: []string{ "actual JSON (object) and expected JSON (null) were of different types at '$'", @@ -189,10 +209,24 @@ func TestContainsf(t *testing.T) { } return } - for i := range tc.msgs { - if exp, got := tc.msgs[i], tp.messages[i]; got != exp { + + if len(tc.msgs) == 1 { + if exp, got := tc.msgs[0], tp.messages[0]; got != exp { st.Errorf("expected assertion message:\n'%s'\nbut got\n'%s'", exp, got) } + return + } + + for _, exp := range tc.msgs { + found := false + for _, got := range tp.messages { + if got == exp { + found = true + } + } + if !found { + st.Errorf("couldn't find expected assertion message:\n'%s'", exp) + } } }) } From c5277de4aed3419fecc96ee42e2ed25098e5ae0b Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:29:14 +0900 Subject: [PATCH 11/14] Add comprehensive contains test --- integration_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_test.go b/integration_test.go index c9117c5..0e17d53 100644 --- a/integration_test.go +++ b/integration_test.go @@ -187,6 +187,7 @@ not found in {name: "expected any value, but got null", act: `{"foo": null}`, exp: `{"foo": "<>"}`, msgs: []string{ "expected the presence of any value at '$.foo', but was absent", }}, + {name: "unordered multi-element array of different types contains subset", act: `["alpha", 5, false, ["foo"], {"bar": "baz"}]`, exp: `["<>", 5, "alpha", {"bar": "baz"}]`, msgs: nil}, } for _, tc := range tt { t.Run(tc.name, func(st *testing.T) { From e7fda99d3cbdc68d311231d8a4429fcaabc9d816 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:45:02 +0900 Subject: [PATCH 12/14] Decide that ordered arrays are contains-able --- array.go | 50 ++++++++++++++++++++++----------------------- integration_test.go | 11 ++++++++++ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/array.go b/array.go index dac8cb6..d072b3a 100644 --- a/array.go +++ b/array.go @@ -63,39 +63,39 @@ func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) { if len(act) < len(exp) { a.tt.Errorf("length of expected array at '%s' was longer (length %d) than the actual array (length %d)", path, len(exp), len(act)) serializedAct, serializedExp := serialize(act), serialize(exp) - if len(serializedAct+serializedExp) < 50 { - a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON to contain: %+v", path, serializedAct, serializedExp) - } else { - a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON to contain:\n%+v", path, serializedAct, serializedExp) - } + a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON to contain: %+v", path, serializedAct, serializedExp) return } if unordered { - mismatchedExpPaths := map[string]string{} - for i := range exp { - found := false - serializedExp := serialize(exp[i]) - for j := range act { - ap := arrayPrinter{} - serializedAct := serialize(act[j]) - New(&ap).pathContainsf("", serializedAct, serializedExp) - if len(ap) == 0 { - found = true - } - } - if !found { - mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<>" + a.checkContainsUnorderedArray(path, act, exp) + return + } + for i := range exp { + a.pathContainsf(fmt.Sprintf("%s[%d]", path, i), serialize(act[i]), serialize(exp[i])) + } +} + +func (a *Asserter) checkContainsUnorderedArray(path string, act, exp []interface{}) { + mismatchedExpPaths := map[string]string{} + for i := range exp { + found := false + serializedExp := serialize(exp[i]) + for j := range act { + ap := arrayPrinter{} + serializedAct := serialize(act[j]) + New(&ap).pathContainsf("", serializedAct, serializedExp) + if len(ap) == 0 { + found = true } } - for path, serializedExp := range mismatchedExpPaths { - a.tt.Errorf("element at %s in the expected payload was not found anywhere in the actual JSON array:\n%s\nnot found in\n%s", path, serializedExp, serialize(act)) - } - } else { - for i := range exp { - a.pathContainsf(fmt.Sprintf("%s[%d]", path, i), serialize(act[i]), serialize(exp[i])) + if !found { + mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<>" } } + for path, serializedExp := range mismatchedExpPaths { + a.tt.Errorf("element at %s in the expected payload was not found anywhere in the actual JSON array:\n%s\nnot found in\n%s", path, serializedExp, serialize(act)) + } } type arrayPrinter []string diff --git a/integration_test.go b/integration_test.go index 0e17d53..8d2917c 100644 --- a/integration_test.go +++ b/integration_test.go @@ -181,6 +181,17 @@ not found in ["alpha","beta","gamma"]`, }}, + {name: "multi-element array contains itself", act: `["alpha", "beta"]`, exp: `["alpha", "beta"]`, msgs: nil}, + {name: "multi-element array does not contain itself permuted", act: `["alpha", "beta"]`, exp: `["beta" ,"alpha"]`, msgs: []string{ + "expected string at '$[0]' to be 'beta' but was 'alpha'", + "expected string at '$[1]' to be 'alpha' but was 'beta'", + }}, + + // Allow users to test against a subset of the payload without erroring out. + // This is to avoid the frustraion and unintuitive solution of adding "<>" in order to "enable" subsetting, + // which is really implied with the `contains` part of the API name. + {name: "multi-element array does not contain its subset", act: `["alpha", "beta"]`, exp: `["alpha"]`, msgs: []string{}}, + {name: "expected and actual have different types", act: `{"foo": "bar"}`, exp: `null`, msgs: []string{ "actual JSON (object) and expected JSON (null) were of different types at '$'", }}, From 2542bc5b378ac1fbd664c4704d6090e234c48df7 Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:53:26 +0900 Subject: [PATCH 13/14] Add (commented) big fat test --- integration_test.go | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/integration_test.go b/integration_test.go index 8d2917c..20ffb5b 100644 --- a/integration_test.go +++ b/integration_test.go @@ -150,7 +150,6 @@ func TestContainsf(t *testing.T) { {name: "boolean does not contain a different boolean", act: `true`, exp: `false`, msgs: []string{ "expected boolean at '$' to be false but was true", }}, - {name: "empty array contains empty array", act: `[]`, exp: `[]`, msgs: nil}, {name: "single-element array contains empty array", act: `["fish"]`, exp: `[]`, msgs: nil}, {name: "unordered empty array contains empty array", act: `[]`, exp: `["<>"]`, msgs: nil}, @@ -180,18 +179,19 @@ not found in not found in ["alpha","beta","gamma"]`, }}, - {name: "multi-element array contains itself", act: `["alpha", "beta"]`, exp: `["alpha", "beta"]`, msgs: nil}, {name: "multi-element array does not contain itself permuted", act: `["alpha", "beta"]`, exp: `["beta" ,"alpha"]`, msgs: []string{ "expected string at '$[0]' to be 'beta' but was 'alpha'", "expected string at '$[1]' to be 'alpha' but was 'beta'", }}, - // Allow users to test against a subset of the payload without erroring out. // This is to avoid the frustraion and unintuitive solution of adding "<>" in order to "enable" subsetting, // which is really implied with the `contains` part of the API name. - {name: "multi-element array does not contain its subset", act: `["alpha", "beta"]`, exp: `["alpha"]`, msgs: []string{}}, - + {name: "multi-element array does contain its subset", act: `["alpha", "beta"]`, exp: `["alpha"]`, msgs: []string{}}, + {name: "multi-element array does not contain its superset", act: `["alpha", "beta"]`, exp: `["alpha", "beta", "gamma"]`, msgs: []string{ + "length of expected array at '$' was longer (length 3) than the actual array (length 2)", + `actual JSON at '$' was: ["alpha","beta"], but expected JSON to contain: ["alpha","beta","gamma"]`, + }}, {name: "expected and actual have different types", act: `{"foo": "bar"}`, exp: `null`, msgs: []string{ "actual JSON (object) and expected JSON (null) were of different types at '$'", }}, @@ -199,6 +199,40 @@ not found in "expected the presence of any value at '$.foo', but was absent", }}, {name: "unordered multi-element array of different types contains subset", act: `["alpha", 5, false, ["foo"], {"bar": "baz"}]`, exp: `["<>", 5, "alpha", {"bar": "baz"}]`, msgs: nil}, + + {name: "object contains its subset", act: `{"foo": "bar", "alpha": "omega"}`, exp: `{"alpha": "omega"}`, msgs: nil}, + /* + { + name: "big fat test", + act: `{ + "arr": [ + "alpha", + 5, + false, + ["foo"], + { + "bar": "baz", + "fork": { + "start": "stop" + }, + "nested": ["really", "fast"] + } + ], + "fish": "mooney" + }`, + exp: `{ + "arr": [ + "<>", + 5, + { + "fork": { + "start": "stop" + }, + "nested": ["fast"] + } + ] + }`, msgs: nil}, + */ } for _, tc := range tt { t.Run(tc.name, func(st *testing.T) { From 0ed2ee67fc3834f9042b7765489310b46ed8dfea Mon Sep 17 00:00:00 2001 From: Roger Guldbrandsen Date: Sun, 7 Mar 2021 19:58:20 +0900 Subject: [PATCH 14/14] Improve contains object logic --- object.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/object.go b/object.go index d9344a3..de3b617 100644 --- a/object.go +++ b/object.go @@ -27,11 +27,8 @@ func (a *Asserter) checkObject(path string, act, exp map[string]interface{}) { func (a *Asserter) checkContainsObject(path string, act, exp map[string]interface{}) { a.tt.Helper() - if len(act) > len(exp) { - a.tt.Errorf("expected %d keys at '%s' but only got %d keys", len(exp), path, len(act)) - } - if unique := difference(exp, act); len(unique) != 0 { - a.tt.Errorf("expected object key(s) %+v missing at '%s'", serialize(unique), path) + if missingExpected := difference(exp, act); len(missingExpected) != 0 { + a.tt.Errorf("expected object key(s) %+v missing at '%s'", serialize(missingExpected), path) } for key := range exp { if contains(act, key) { @@ -40,10 +37,11 @@ func (a *Asserter) checkContainsObject(path string, act, exp map[string]interfac } } -func difference(act, exp map[string]interface{}) []string { +// difference returns a slice of the keys that were found in a but not in b. +func difference(a, b map[string]interface{}) []string { unique := []string{} - for key := range act { - if !contains(exp, key) { + for key := range a { + if !contains(b, key) { unique = append(unique, key) } }