Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>>" {
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)
Expand All @@ -18,11 +25,86 @@ func (a *Asserter) checkArray(path string, act, exp []interface{}) {
}
return
}

if unordered {
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
}
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)
}
}
}

func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) {
a.tt.Helper()

var unordered bool
if len(exp) > 0 && exp[0] == "<<UNORDERED>>" {
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)
a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON to contain: %+v", path, serializedAct, serializedExp)
return
}

if unordered {
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
}
}
if !found {
mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<<UNORDERED>>"
}
}
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

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 {
Expand Down
60 changes: 54 additions & 6 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package jsonassert

import (
"encoding/json"
"errors"
"fmt"
"strings"
)
Expand Down Expand Up @@ -59,13 +58,62 @@ func (a *Asserter) pathassertf(path, act, exp string) {
}
}

func serialize(a interface{}) string {
bytes, err := json.Marshal(a)
func (a *Asserter) pathContainsf(path, act, exp string) {
a.tt.Helper()
if act == exp {
return
}
actType, err := findType(act)
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()))
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 == "<<PRESENCE>>" {
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 expType {
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)
case jsonNull:
// Intentionally don't check as it wasn't expected in the payload
}
}

func serialize(a interface{}) string {
bytes, _ := json.Marshal(a)
return string(bytes)
}

Expand Down
7 changes: 7 additions & 0 deletions exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ func (a *Asserter) Assertf(actualJSON, expectedJSON string, fmtArgs ...interface
a.tt.Helper()
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...))
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module github.com/kinbiko/jsonassert

go 1.12
162 changes: 162 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: `["<<UNORDERED>>", "world", "hello"]`, msgs: []string{}},
{name: "different non-empty unsorted arrays", act: `["hello", "world"]`, exp: `["<<UNORDERED>>", "世界", "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"]`,
Expand Down Expand Up @@ -117,6 +125,160 @@ but expected JSON was:
}
}

func TestContainsf(t *testing.T) {
tt := []struct {
name string
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: `["<<UNORDERED>>"]`, msgs: nil},
{name: "unordered single-element array contains empty array", act: `["fish"]`, exp: `["<<UNORDERED>>"]`, 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: `["<<UNORDERED>>", "beta", "alpha"]`, msgs: nil},
{name: "unordered multi-element array does not contain single element", act: `["alpha", "beta", "gamma"]`, exp: `["<<UNORDERED>>", "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: `["<<UNORDERED>>", "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: "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 "<<UNORDERED>>" in order to "enable" subsetting,
// which is really implied with the `contains` part of the API name.
{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 '$'",
}},
{name: "expected any value, but got null", act: `{"foo": null}`, exp: `{"foo": "<<PRESENCE>>"}`, 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: `["<<UNORDERED>>", 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": [
"<<UNORDERED>>",
5,
{
"fork": {
"start": "stop"
},
"nested": ["fast"]
}
]
}`, msgs: nil},
*/
}
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
}

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)
}
}
})
}

}

func setup() (*testPrinter, *jsonassert.Asserter) {
tp := &testPrinter{}
return tp, jsonassert.New(tp)
Expand Down
20 changes: 17 additions & 3 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,24 @@ func (a *Asserter) checkObject(path string, act, exp map[string]interface{}) {
}
}

func difference(act, exp map[string]interface{}) []string {
func (a *Asserter) checkContainsObject(path string, act, exp map[string]interface{}) {
a.tt.Helper()

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) {
a.pathContainsf(path+"."+key, serialize(act[key]), serialize(exp[key]))
}
}
}

// 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)
}
}
Expand Down