Skip to content

Commit

Permalink
Add support for unordered arrays (#30)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
kinbiko authored Aug 21, 2021
1 parent ba5ca9f commit 16c9aa8
Show file tree
Hide file tree
Showing 7 changed files with 1,135 additions and 27 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ func TestWhatever(t *testing.T) {

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

### Ignore ordering in arrays

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:

```go
func TestUnorderedArray(t *testing.T) {
ja := jsonassert.New(t)
payload := `["bar", "foo", "baz"]`
ja.Assertf(payload, `["foo", "bar", "baz"]`) // Order matters, will fail your test.
ja.Assertf(payload, `["<<UNORDERED>>", "foo", "bar", "baz"]`) // Order agnostic, will pass your test.
}
```

## Docs

You can find the [GoDocs for this package here](https://pkg.go.dev/github.com/kinbiko/jsonassert).
Expand Down
68 changes: 65 additions & 3 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,71 @@ import (
)

func (a *Asserter) checkArray(path string, act, exp []interface{}) {
a.tt.Helper()
if len(exp) > 0 && exp[0] == "<<UNORDERED>>" {
a.checkArrayUnordered(path, act, exp[1:])
} else {
a.checkArrayOrdered(path, act, exp)
}
}

func (a *Asserter) checkArrayUnordered(path string, act, exp []interface{}) {
a.tt.Helper()
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)
if len(serializedAct+serializedExp) < 50 {
a.tt.Errorf("actual JSON at '%s' was: %+v, but expected JSON was: %+v, potentially in a different order", path, serializedAct, serializedExp)
} else {
a.tt.Errorf("actual JSON at '%s' was:\n%+v\nbut expected JSON was:\n%+v,\npotentially in a different order", path, serializedAct, serializedExp)
}
return
}

for i, actEl := range act {
found := false
for _, expEl := range exp {
if a.deepEqual(actEl, expEl) {
found = true
}
}
if !found {
serializedEl := serialize(actEl)
if len(serializedEl) < 50 {
a.tt.Errorf("actual JSON at '%s[%d]' contained an unexpected element: %s", path, i, serializedEl)
} else {
a.tt.Errorf("actual JSON at '%s[%d]' contained an unexpected element:\n%s", path, i, serializedEl)
}
}
}

for i, expEl := range exp {
found := false
for _, actEl := range act {
found = found || a.deepEqual(expEl, actEl)
}
if !found {
serializedEl := serialize(expEl)
if len(serializedEl) < 50 {
a.tt.Errorf("expected JSON at '%s[%d]': %s was missing from actual payload", path, i, serializedEl)
} else {
a.tt.Errorf("expected JSON at '%s[%d]':\n%s\nwas missing from actual payload", path, i, serializedEl)
}
}
}
}

func (a *Asserter) deepEqual(act, exp interface{}) bool {
// There's a non-zero chance that JSON serialization will *not* be
// deterministic in the future like it is in v1.16.
// However, until this is the case, I can't seem to find a test case that
// makes this evaluation return a false positive.
// The benefit is a lot of simplicity and considerable performance benefits
// for large nested structures.
return serialize(act) == serialize(exp)
}

func (a *Asserter) checkArrayOrdered(path string, act, exp []interface{}) {
a.tt.Helper()
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))
Expand All @@ -28,9 +93,6 @@ func extractArray(s string) ([]interface{}, error) {
if len(s) == 0 {
return nil, fmt.Errorf("cannot parse empty string as array")
}
if s[0] != '[' {
return nil, fmt.Errorf("cannot parse '%s' as array", s)
}
var arr []interface{}
err := json.Unmarshal([]byte(s), &arr)
return arr, err
Expand Down
11 changes: 11 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,14 @@ func ExampleAsserter_Assertf_presenceOnly() {
//unexpected object key(s) ["hi"] found at '$'
//expected object key(s) ["hello"] missing at '$'
}

func ExampleAsserter_Assertf_unorderedArray() {
ja := jsonassert.New(t)
ja.Assertf(
`["zero", "one", "two"]`,
`["<<UNORDERED>>", "one", "two", "three"]`,
)
//output:
//actual JSON at '$[0]' contained an unexpected element: "zero"
//expected JSON at '$[2]': "three" was missing from actual payload
}
26 changes: 22 additions & 4 deletions exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,23 @@ along with the "world" format argument. For example:
ja.Assertf(`{"hello": "world"}`, `{"hello":"%s"}`, "world")
Additionally, you may wish to make assertions against the *presence* of a
value, but not against its value. For example:
You may wish to make assertions against the *presence* of a value, but not
against its value. For example:
ja.Assertf(`{"uuid": "94ae1a31-63b2-4a55-a478-47764b60c56b"}`, `{"uuid":"<<PRESENCE>>"}`)
will verify that the UUID field is present, but does not check its actual value.
You may use "<<PRESENCE>>" against any type of value. The only exception is null, which
will result in an assertion failure.
If you don't know / care about the order of the elements in an array in your
payload, you can ignore the ordering:
payload := `["bar", "foo", "baz"]`
ja.Assertf(payload, `["<<UNORDERED>>", "foo", "bar", "baz"]`)
The above will verify that "foo", "bar", and "baz" are exactly the elements in
the payload, but will ignore the order in which they appear.
*/
package jsonassert

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

0 comments on commit 16c9aa8

Please sign in to comment.