Skip to content

Commit 287b375

Browse files
committed
feat: Add Containsf initial stab
1 parent ca09b60 commit 287b375

File tree

5 files changed

+222
-17
lines changed

5 files changed

+222
-17
lines changed

array.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,64 @@ func (a *Asserter) checkArrayOrdered(path string, act, exp []interface{}) {
7979
}
8080
}
8181

82+
func (a *Asserter) checkContainsArray(path string, act, exp []interface{}) {
83+
a.tt.Helper()
84+
85+
var unordered bool
86+
if len(exp) > 0 && exp[0] == "<<UNORDERED>>" {
87+
unordered = true
88+
exp = exp[1:]
89+
}
90+
91+
if len(act) < len(exp) {
92+
a.tt.Errorf("length of expected array at '%s' was longer (length %d) than the actual array (length %d)", path, len(exp), len(act))
93+
serializedAct, serializedExp := serialize(act), serialize(exp)
94+
a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON to contain: %+v", path, serializedAct, serializedExp)
95+
return
96+
}
97+
98+
if unordered {
99+
a.checkContainsUnorderedArray(path, act, exp)
100+
return
101+
}
102+
for i := range exp {
103+
a.pathContainsf(fmt.Sprintf("%s[%d]", path, i), serialize(act[i]), serialize(exp[i]))
104+
}
105+
}
106+
107+
func (a *Asserter) checkContainsUnorderedArray(path string, act, exp []interface{}) {
108+
mismatchedExpPaths := map[string]string{}
109+
for i := range exp {
110+
found := false
111+
serializedExp := serialize(exp[i])
112+
for j := range act {
113+
ap := arrayPrinter{}
114+
serializedAct := serialize(act[j])
115+
New(&ap).pathContainsf("", serializedAct, serializedExp)
116+
if len(ap) == 0 {
117+
found = true
118+
}
119+
}
120+
if !found {
121+
mismatchedExpPaths[fmt.Sprintf("%s[%d]", path, i+1)] = serializedExp // + 1 because 0th element is "<<UNORDERED>>"
122+
}
123+
}
124+
for path, serializedExp := range mismatchedExpPaths {
125+
a.tt.Errorf(`element at %s in the expected payload was not found anywhere in the actual JSON array:
126+
%s
127+
not found in
128+
%s`,
129+
path, serializedExp, serialize(act))
130+
}
131+
}
132+
133+
type arrayPrinter []string
134+
135+
func (p *arrayPrinter) Errorf(msg string, args ...interface{}) {
136+
n := append(*p, fmt.Sprintf(msg, args...))
137+
*p = n
138+
}
139+
82140
func extractArray(s string) ([]interface{}, bool) {
83141
s = strings.TrimSpace(s)
84142
if s == "" {

core.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,59 @@ func (a *Asserter) pathassertf(path, act, exp string) {
6262
}
6363
}
6464

65+
func (a *Asserter) pathContainsf(path, act, exp string) {
66+
a.tt.Helper()
67+
if act == exp {
68+
return
69+
}
70+
actType, err := findType(act)
71+
if err != nil {
72+
a.tt.Errorf("'actual' JSON is not valid JSON: " + err.Error())
73+
return
74+
}
75+
expType, err := findType(exp)
76+
if err != nil {
77+
a.tt.Errorf("'expected' JSON is not valid JSON: " + err.Error())
78+
return
79+
}
80+
81+
// If we're only caring about the presence of the key, then don't bother checking any further
82+
if expPresence, _ := extractString(exp); expPresence == "<<PRESENCE>>" {
83+
if actType == jsonNull {
84+
a.tt.Errorf(`expected the presence of any value at '%s', but was absent`, path)
85+
}
86+
return
87+
}
88+
if actType != expType {
89+
a.tt.Errorf("actual JSON (%s) and expected JSON (%s) were of different types at '%s'", actType, expType, path)
90+
return
91+
}
92+
switch expType {
93+
case jsonBoolean:
94+
actBool, _ := extractBoolean(act)
95+
expBool, _ := extractBoolean(exp)
96+
a.checkBoolean(path, actBool, expBool)
97+
case jsonNumber:
98+
actNumber, _ := extractNumber(act)
99+
expNumber, _ := extractNumber(exp)
100+
a.checkNumber(path, actNumber, expNumber)
101+
case jsonString:
102+
actString, _ := extractString(act)
103+
expString, _ := extractString(exp)
104+
a.checkString(path, actString, expString)
105+
case jsonObject:
106+
actObject, _ := extractObject(act)
107+
expObject, _ := extractObject(exp)
108+
a.checkContainsObject(path, actObject, expObject)
109+
case jsonArray:
110+
actArray, _ := extractArray(act)
111+
expArray, _ := extractArray(exp)
112+
a.checkContainsArray(path, actArray, expArray)
113+
case jsonNull:
114+
// Intentionally don't check as it wasn't expected in the payload
115+
}
116+
}
117+
65118
func serialize(a interface{}) string {
66119
//nolint:errchkjson // Can be confident this won't return an error: the
67120
// input will be a nested part of valid JSON, thus valid JSON

exports.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,10 @@ func (a *Asserter) Assertf(actualJSON, expectedJSON string, fmtArgs ...interface
127127
a.tt.Helper()
128128
a.pathassertf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...))
129129
}
130+
131+
// TODO: remember to document what happens if you call Containsf with a null
132+
// property as currently it will treat it as the key being missing.
133+
func (a *Asserter) Containsf(actualJSON, expectedJSON string, fmtArgs ...interface{}) {
134+
a.tt.Helper()
135+
a.pathContainsf("$", actualJSON, fmt.Sprintf(expectedJSON, fmtArgs...))
136+
}

integration_test.go

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestAssertf(t *testing.T) {
2929
"strings": {`"hello world"`, `"hello world"`, nil},
3030
} {
3131
tc := tc
32-
t.Run(name, func(t *testing.T) { tc.check(t) })
32+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
3333
}
3434
})
3535

@@ -45,7 +45,7 @@ func TestAssertf(t *testing.T) {
4545
"empty v non-empty string": {`""`, `"world"`, []string{`expected string at '$' to be 'world' but was ''`}},
4646
} {
4747
tc := tc
48-
t.Run(name, func(t *testing.T) { tc.check(t) })
48+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
4949
}
5050
})
5151
})
@@ -83,7 +83,7 @@ func TestAssertf(t *testing.T) {
8383
},
8484
} {
8585
tc := tc
86-
t.Run(name, func(t *testing.T) { tc.check(t) })
86+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
8787
}
8888
})
8989

@@ -113,7 +113,7 @@ func TestAssertf(t *testing.T) {
113113
},
114114
} {
115115
tc := tc
116-
t.Run(name, func(t *testing.T) { tc.check(t) })
116+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
117117
}
118118
})
119119

@@ -152,7 +152,7 @@ func TestAssertf(t *testing.T) {
152152
},
153153
} {
154154
tc := tc
155-
t.Run(name, func(t *testing.T) { tc.check(t) })
155+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
156156
}
157157
})
158158
})
@@ -196,17 +196,14 @@ but expected JSON was:
196196
`["world"]`,
197197
[]string{`expected string at '$[0]' to be 'world' but was 'hello'`},
198198
},
199-
"different length non-empty arrays": {
199+
"identical non-empty unsorted arrays": {
200200
`["hello", "world"]`,
201-
`["world"]`,
202-
[]string{
203-
`length of arrays at '$' were different. Expected array to be of length 1, but contained 2 element(s)`,
204-
`actual JSON at '$' was: ["hello","world"], but expected JSON was: ["world"]`,
205-
},
201+
`["<<UNORDERED>>", "world", "hello"]`,
202+
[]string{},
206203
},
207204
} {
208205
tc := tc
209-
t.Run(name, func(t *testing.T) { tc.check(t) })
206+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
210207
}
211208
})
212209

@@ -248,7 +245,7 @@ but expected JSON was:
248245
},
249246
} {
250247
tc := tc
251-
t.Run(name, func(t *testing.T) { tc.check(t) })
248+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
252249
}
253250
})
254251

@@ -314,7 +311,7 @@ but expected JSON was:
314311
},
315312
} {
316313
tc := tc
317-
t.Run(name, func(t *testing.T) { tc.check(t) })
314+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
318315
}
319316
})
320317
})
@@ -346,7 +343,7 @@ potentially in a different order`,
346343
},
347344
} {
348345
tc := tc
349-
t.Run(name, func(t *testing.T) { tc.check(t) })
346+
t.Run(name, func(t *testing.T) { tc.checkAssertf(t) })
350347
}
351348
})
352349

@@ -449,20 +446,96 @@ but was
449446
was missing from actual payload`,
450447
},
451448
}
452-
tc.check(t)
449+
tc.checkAssertf(t)
453450
})
454451
}
455452

453+
func TestContainsf(t *testing.T) {
454+
t.Parallel()
455+
tt := map[string]*testCase{
456+
"actual not valid json": {
457+
`foo`,
458+
`"foo"`,
459+
[]string{`'actual' JSON is not valid JSON: unable to identify JSON type of "foo"`},
460+
},
461+
"expected not valid json": {`"foo"`, `foo`, []string{`'expected' JSON is not valid JSON: unable to identify JSON type of "foo"`}},
462+
"number contains a number": {`5`, `5`, nil},
463+
"number does not contain a different number": {`5`, `-2`, []string{"expected number at '$' to be '-2.0000000' but was '5.0000000'"}},
464+
"string contains a string": {`"foo"`, `"foo"`, nil},
465+
"string does not contain a different string": {`"foo"`, `"bar"`, []string{"expected string at '$' to be 'bar' but was 'foo'"}},
466+
"boolean contains a boolean": {`true`, `true`, nil},
467+
"boolean does not contain a different boolean": {`true`, `false`, []string{"expected boolean at '$' to be false but was true"}},
468+
"empty array contains empty array": {`[]`, `[]`, nil},
469+
"single-element array contains empty array": {`["fish"]`, `[]`, nil},
470+
"unordered empty array contains empty array": {`[]`, `["<<UNORDERED>>"]`, nil},
471+
"unordered single-element array contains empty array": {`["fish"]`, `["<<UNORDERED>>"]`, nil},
472+
"empty array contains single-element array": {`[]`, `["fish"]`, []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"]`}},
473+
"unordered multi-element array contains subset": {`["alpha", "beta", "gamma"]`, `["<<UNORDERED>>", "beta", "alpha"]`, nil},
474+
"unordered multi-element array does not contain single element": {`["alpha", "beta", "gamma"]`, `["<<UNORDERED>>", "delta", "alpha"]`, []string{
475+
`element at $[1] in the expected payload was not found anywhere in the actual JSON array:
476+
"delta"
477+
not found in
478+
["alpha","beta","gamma"]`,
479+
}},
480+
"unordered multi-element array contains none of multi-element array": {`["alpha", "beta", "gamma"]`, `["<<UNORDERED>>", "delta", "pi", "omega"]`, []string{
481+
`element at $[1] in the expected payload was not found anywhere in the actual JSON array:
482+
"delta"
483+
not found in
484+
["alpha","beta","gamma"]`,
485+
`element at $[2] in the expected payload was not found anywhere in the actual JSON array:
486+
"pi"
487+
not found in
488+
["alpha","beta","gamma"]`,
489+
`element at $[3] in the expected payload was not found anywhere in the actual JSON array:
490+
"omega"
491+
not found in
492+
["alpha","beta","gamma"]`,
493+
}},
494+
"multi-element array contains itself": {`["alpha", "beta"]`, `["alpha", "beta"]`, nil},
495+
"multi-element array does not contain itself permuted": {`["alpha", "beta"]`, `["beta" ,"alpha"]`, []string{
496+
"expected string at '$[0]' to be 'beta' but was 'alpha'",
497+
"expected string at '$[1]' to be 'alpha' but was 'beta'",
498+
}},
499+
// Allow users to test against a subset of the payload without erroring out.
500+
// This is to avoid the frustraion and unintuitive solution of adding "<<UNORDERED>>" in order to "enable" subsetting,
501+
// which is really implied with the `contains` part of the API name.
502+
"multi-element array does contain its subset": {`["alpha", "beta"]`, `["alpha"]`, []string{}},
503+
"multi-element array does not contain its superset": {`["alpha", "beta"]`, `["alpha", "beta", "gamma"]`, []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"]`}},
504+
"expected and actual have different types": {`{"foo": "bar"}`, `null`, []string{"actual JSON (object) and expected JSON (null) were of different types at '$'"}},
505+
"expected any value but got null": {`{"foo": null}`, `{"foo": "<<PRESENCE>>"}`, []string{"expected the presence of any value at '$.foo', but was absent"}},
506+
"unordered multi-element array of different types contains subset": {`["alpha", 5, false, ["foo"], {"bar": "baz"}]`, `["<<UNORDERED>>", 5, "alpha", {"bar": "baz"}]`, nil},
507+
"object contains its subset": {`{"foo": "bar", "alpha": "omega"}`, `{"alpha": "omega"}`, nil},
508+
}
509+
for name, tc := range tt {
510+
tc := tc
511+
t.Run(name, func(t *testing.T) {
512+
t.Parallel()
513+
tc.checkContainsf(t)
514+
})
515+
}
516+
}
517+
456518
type testCase struct {
457519
act, exp string
458520
msgs []string
459521
}
460522

461-
func (tc *testCase) check(t *testing.T) {
523+
func (tc *testCase) checkContainsf(t *testing.T) {
524+
t.Helper()
525+
tp := &testPrinter{messages: nil}
526+
jsonassert.New(tp).Containsf(tc.act, tc.exp)
527+
tc.check(t, tp)
528+
}
529+
530+
func (tc *testCase) checkAssertf(t *testing.T) {
462531
t.Helper()
463532
tp := &testPrinter{messages: nil}
464533
jsonassert.New(tp).Assertf(tc.act, tc.exp)
534+
tc.check(t, tp)
535+
}
465536

537+
func (tc *testCase) check(t *testing.T, tp *testPrinter) {
538+
t.Helper()
466539
if got := len(tp.messages); got != len(tc.msgs) {
467540
t.Errorf("expected %d assertion message(s) but got %d", len(tc.msgs), got)
468541
}

object.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ func (a *Asserter) checkObject(path string, act, exp map[string]interface{}) {
2323
}
2424
}
2525

26+
func (a *Asserter) checkContainsObject(path string, act, exp map[string]interface{}) {
27+
a.tt.Helper()
28+
29+
if missingExpected := difference(exp, act); len(missingExpected) != 0 {
30+
a.tt.Errorf("expected object key(s) %+v missing at '%s'", serialize(missingExpected), path)
31+
}
32+
for key := range exp {
33+
if contains(act, key) {
34+
a.pathContainsf(path+"."+key, serialize(act[key]), serialize(exp[key]))
35+
}
36+
}
37+
}
38+
39+
// difference returns a slice of the keys that were found in a but not in b.
2640
func difference(act, exp map[string]interface{}) []string {
2741
unique := []string{}
2842
for key := range act {

0 commit comments

Comments
 (0)