Skip to content

Commit 16c9aa8

Browse files
authored
Add support for unordered arrays (#30)
* feat: Add support for unordered arrays - Use the "<<UNORDERED>>" directive as the first string in the unordered array to ignore ordering when comparing arrays. - Adds unit tests for this new logic. - Adds a big real-world payload integration test that should cover most of the features of this package in combination * fix: Remove snippet of unreachable code * docs: Explain <<UNORDERED>> in README * docs: Explain <<UNORDERED>> in GoDocs * docs: Add <<UNORDERED>> runnable example * chore: Address self-review comments (#30)
1 parent ba5ca9f commit 16c9aa8

7 files changed

Lines changed: 1135 additions & 27 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ func TestWhatever(t *testing.T) {
8383

8484
The above will fail your tests because the `time` key was not present in the actual JSON, and the `uuid` was `null`.
8585

86+
### Ignore ordering in arrays
87+
88+
If your JSON payload contains an array with elements whose ordering is not deterministic, then you can use the `"<<UNORDERED>>"` directive as the first element of the array in question:
89+
90+
```go
91+
func TestUnorderedArray(t *testing.T) {
92+
ja := jsonassert.New(t)
93+
payload := `["bar", "foo", "baz"]`
94+
ja.Assertf(payload, `["foo", "bar", "baz"]`) // Order matters, will fail your test.
95+
ja.Assertf(payload, `["<<UNORDERED>>", "foo", "bar", "baz"]`) // Order agnostic, will pass your test.
96+
}
97+
```
98+
8699
## Docs
87100

88101
You can find the [GoDocs for this package here](https://pkg.go.dev/github.com/kinbiko/jsonassert).

array.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,71 @@ import (
77
)
88

99
func (a *Asserter) checkArray(path string, act, exp []interface{}) {
10+
a.tt.Helper()
11+
if len(exp) > 0 && exp[0] == "<<UNORDERED>>" {
12+
a.checkArrayUnordered(path, act, exp[1:])
13+
} else {
14+
a.checkArrayOrdered(path, act, exp)
15+
}
16+
}
17+
18+
func (a *Asserter) checkArrayUnordered(path string, act, exp []interface{}) {
19+
a.tt.Helper()
20+
if len(act) != len(exp) {
21+
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))
22+
serializedAct, serializedExp := serialize(act), serialize(exp)
23+
if len(serializedAct+serializedExp) < 50 {
24+
a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON was: %+v, potentially in a different order", path, serializedAct, serializedExp)
25+
} else {
26+
a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON was:\n%+v,\npotentially in a different order", path, serializedAct, serializedExp)
27+
}
28+
return
29+
}
30+
31+
for i, actEl := range act {
32+
found := false
33+
for _, expEl := range exp {
34+
if a.deepEqual(actEl, expEl) {
35+
found = true
36+
}
37+
}
38+
if !found {
39+
serializedEl := serialize(actEl)
40+
if len(serializedEl) < 50 {
41+
a.tt.Errorf("actual JSON at '%s[%d]' contained an unexpected element: %s", path, i, serializedEl)
42+
} else {
43+
a.tt.Errorf("actual JSON at '%s[%d]' contained an unexpected element:\n%s", path, i, serializedEl)
44+
}
45+
}
46+
}
47+
48+
for i, expEl := range exp {
49+
found := false
50+
for _, actEl := range act {
51+
found = found || a.deepEqual(expEl, actEl)
52+
}
53+
if !found {
54+
serializedEl := serialize(expEl)
55+
if len(serializedEl) < 50 {
56+
a.tt.Errorf("expected JSON at '%s[%d]': %s was missing from actual payload", path, i, serializedEl)
57+
} else {
58+
a.tt.Errorf("expected JSON at '%s[%d]':\n%s\nwas missing from actual payload", path, i, serializedEl)
59+
}
60+
}
61+
}
62+
}
63+
64+
func (a *Asserter) deepEqual(act, exp interface{}) bool {
65+
// There's a non-zero chance that JSON serialization will *not* be
66+
// deterministic in the future like it is in v1.16.
67+
// However, until this is the case, I can't seem to find a test case that
68+
// makes this evaluation return a false positive.
69+
// The benefit is a lot of simplicity and considerable performance benefits
70+
// for large nested structures.
71+
return serialize(act) == serialize(exp)
72+
}
73+
74+
func (a *Asserter) checkArrayOrdered(path string, act, exp []interface{}) {
1075
a.tt.Helper()
1176
if len(act) != len(exp) {
1277
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))
@@ -28,9 +93,6 @@ func extractArray(s string) ([]interface{}, error) {
2893
if len(s) == 0 {
2994
return nil, fmt.Errorf("cannot parse empty string as array")
3095
}
31-
if s[0] != '[' {
32-
return nil, fmt.Errorf("cannot parse '%s' as array", s)
33-
}
3496
var arr []interface{}
3597
err := json.Unmarshal([]byte(s), &arr)
3698
return arr, err

examples_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,14 @@ func ExampleAsserter_Assertf_presenceOnly() {
4444
//unexpected object key(s) ["hi"] found at '$'
4545
//expected object key(s) ["hello"] missing at '$'
4646
}
47+
48+
func ExampleAsserter_Assertf_unorderedArray() {
49+
ja := jsonassert.New(t)
50+
ja.Assertf(
51+
`["zero", "one", "two"]`,
52+
`["<<UNORDERED>>", "one", "two", "three"]`,
53+
)
54+
//output:
55+
//actual JSON at '$[0]' contained an unexpected element: "zero"
56+
//expected JSON at '$[2]': "three" was missing from actual payload
57+
}

exports.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,23 @@ along with the "world" format argument. For example:
2020
2121
ja.Assertf(`{"hello": "world"}`, `{"hello":"%s"}`, "world")
2222
23-
Additionally, you may wish to make assertions against the *presence* of a
24-
value, but not against its value. For example:
23+
You may wish to make assertions against the *presence* of a value, but not
24+
against its value. For example:
2525
2626
ja.Assertf(`{"uuid": "94ae1a31-63b2-4a55-a478-47764b60c56b"}`, `{"uuid":"<<PRESENCE>>"}`)
2727
2828
will verify that the UUID field is present, but does not check its actual value.
2929
You may use "<<PRESENCE>>" against any type of value. The only exception is null, which
3030
will result in an assertion failure.
31+
32+
If you don't know / care about the order of the elements in an array in your
33+
payload, you can ignore the ordering:
34+
35+
payload := `["bar", "foo", "baz"]`
36+
ja.Assertf(payload, `["<<UNORDERED>>", "foo", "bar", "baz"]`)
37+
38+
The above will verify that "foo", "bar", and "baz" are exactly the elements in
39+
the payload, but will ignore the order in which they appear.
3140
*/
3241
package jsonassert
3342

@@ -97,14 +106,23 @@ format-directive.
97106
98107
ja.Assertf(`{"averageTestScore": "99%"}`, `{"averageTestScore":"%s"}`, "99%")
99108
100-
Additionally, you may wish to make assertions against the *presence* of a
101-
value, but not against its value. For example:
109+
You may wish to make assertions against the *presence* of a value, but not
110+
against its value. For example:
102111
103112
ja.Assertf(`{"uuid": "94ae1a31-63b2-4a55-a478-47764b60c56b"}`, `{"uuid":"<<PRESENCE>>"}`)
104113
105114
will verify that the UUID field is present, but does not check its actual value.
106115
You may use "<<PRESENCE>>" against any type of value. The only exception is null, which
107116
will result in an assertion failure.
117+
118+
If you don't know / care about the order of the elements in an array in your
119+
payload, you can ignore the ordering:
120+
121+
payload := `["bar", "foo", "baz"]`
122+
ja.Assertf(payload, `["<<UNORDERED>>", "foo", "bar", "baz"]`)
123+
124+
The above will verify that "foo", "bar", and "baz" are exactly the elements in
125+
the payload, but will ignore the order in which they appear.
108126
*/
109127
func (a *Asserter) Assertf(actualJSON, expectedJSON string, fmtArgs ...interface{}) {
110128
a.tt.Helper()

0 commit comments

Comments
 (0)