How to compare arrays ignoring the order of the elements in Jest?

Asked

Viewed 109 times

4

In a situation where I have two arrays, it is possible to use the expect of Jest for a comparison where I hope arrays are equal, regardless of the order of the elements?

I searched, but found no method ready for it.

The comparison I currently use is the .toEqual, which works only if the order of the array elements is the same:

const array = [{ name: 'Bob' }, { name: 'Alice' }];
expect(array).toEqual([{ name: 'Bob' }, { name: 'Alice' }]); // OK
expect(array).toEqual([{ name: 'Alice' }, { name: 'Bob' }]); // Erro
  • There is a simple solution using Jest’s expansion pack jest-Extended. The use of this package would be within the scope of your question?

  • Yeah, @Augustovasques, super valid.

3 answers

5

Compare already ordered arrays

The most obvious alternative is to use the sort to make arrays be compared in the same order. In this case, order by property name:

const sortByName = (os) => os.sort((a, b) => a.name.localeCompare(b.name));

const array = [{ name: 'Bob' }, { name: 'Alice' }];
const expected = [{ name: 'Alice' }, { name: 'Bob' }];

expect(sortByName(array)).toEqual(sortByName(expected)); // Ok

In this case, the comparison is made with keys like string, but any stable sort criteria would work.

Using a container in which the order is not a comparison criterion

Another option is to convert the array to Set, where order is not a comparison criterion:

const array = [{ name: 'Bob' }, { name: 'Alice' }];
const expected = [{ name: 'Alice' }, { name: 'Bob' }];

expect(new Set(array)).toEqual(new Set(expected)); // Ok

The problem with that is that the set, because it does not allow duplicated elements, it would consider two arrays with a different number of equal elements for the Set. To fix this, you also need to compare the arrays by their length.

const array = ['b', 'b', 'a'];
const expected = ['a', 'b'];

expect(new Set(array)).toEqual(new Set(expected)); // Ok
expect(array).toHaveLength(expected.length); // Falha justamente

expect.arrayContaining

Or use the method arrayContaining, as suggested by another answer, but in that case, just as we did in the set, also have to look at the length of the array:

const array = ['b', 'b', 'a'];
const expected = ['a', 'b'];

expect(array).toEqual(expect.arrayContaining(expected)); // Ok
expect(array).toHaveLength(expected.length); // Falha justamente
  • 1

    Between the Set and the array, The second option seems simpler, since it doesn’t need the conversion. The ordination is interesting, and as I commented in the other answer, I would give to create a matcher customized, would only be more complex by needing more arguments (if it is an object, you would need to specify a property to sort).

  • 1

    I went to research the code of arrayContaining out of curiosity, to see how he did it, and he walks through the two arrays (with .every and .some) to find an equivalence. A little better than ordering, but it needs the .toHaveLength, like you said :)

  • 1

    To future visitors: I just noticed a detail that wasn’t mentioned, if you use toHaveLength and arrayContaining to compare [1,2] with [1,1], will not work as expected (the test will pass, [1,1] has the same size and is contained in [1,2]). Then, among the options of this answer, order or use the Set are better.

3

Use a combination with the arrayContaining within the toEqual:

const array = [{ name: 'Bob' }, { name: 'Alice' }]

expect(array).toEqual(
  expect.arrayContaining([{ name: 'Alice' }, { name: 'Bob' }])
)

It would ignore the array order in this case.

Or maybe try to sort both arrays before comparing to the toEqual.

Some details

As described in the documentation on arrayContaining:

expect.arrayContaining(array) corresponds to a received array that contains all elements in the expected array. That is, the expected array is a subset of the received array. Therefore, it combines with a received array that contains elements that are not in the expected array.

"That is, the expected array is a subset of the received array" - This implies that it is sufficient that the expected array has certain values of the tested array for the pass test. This behavior is similar to expect.objectContaining where you do not need all the keys to be present in the object, only a few checked in the test are enough to pass. But what about the case where the array is the same, ie, same size, but disordered? A test like the below would pass without problems:

it.only('array test', () => {
  const array = [{ name: 'Bob' }, { name: 'Alice' }, { name: 'Foo' }]

  expect(array).toEqual(
    expect.arrayContaining([{ name: 'Alice' }, { name: 'Bob' }])
  )
})

Notice that

  • [{ name: 'Alice' }, { name: 'Bob' }]

is a subset of

  • [{ name: 'Bob' }, { name: 'Alice' }, { name: 'Foo' }]

This test should fail because it would be missing { name: 'Foo' } in the expect.arrayContaining.

How to solve

In the most imperative and simple way, compare the size of the arrays using toHaveLength() before checking with the expect.arrayContaining:

// funcao utilitária para reuso em todos os testes
function checkArraysInAnyOrder (array: any[], expected: any[]) {
  expect(array).toHaveLength(expected.length)
  expect(array).toEqual(expect.arrayContaining(expected))
}

Success:

it('array test', () => {
  const array = [{ name: 'Bob' }, { name: 'Alice' }]
  const expected = [{ name: 'Alice' }, { name: 'Bob' }]

  checkArraysInAnyOrder(array, expected) // sucesso
})

Glitch:

it('array test', () => {
  const array = [{ name: 'Bob' }, { name: 'Alice' }, { name: 'Foo' }]
  const expected = [{ name: 'Alice' }, { name: 'Bob' }]

  checkArraysInAnyOrder(array, expected) // falha
})

As discussed in the comments, checkArraysInAnyOrder could be a custom matcher, but I think the effort and complexity wouldn’t be worth it. Using community libraries that extend Jest’s resources like jest-Extended, could help to reach the goal. This, inclusive, has the method toIncludeSameMembers that could be used to fulfill the goal:

Use .toIncludeSameMembers checking if two arrays contain Equal values, in any order.

test('passes when arrays match in a different order', () => {
  expect([1, 2, 3]).toIncludeSameMembers([3, 1, 2]);
  expect([{ foo: 'bar' }, { baz: 'qux' }]).toIncludeSameMembers([{ baz: 'qux' }, { foo: 'bar' }]);
});

Of course it is up to the discussion and analysis if it is worth adding an entire library just for the use of a specific functionality. Still talking about this library, you could base, or copy, the source code that implements the toIncludeSameMembers and create your own match.

  • 1

    It’s actually a very interesting option, I can think of creating a matcher customized for this, but checking the length of the array, as Luiz Felipe said. I think it’s important that you mention this detail about the size in your answer, since expect([1,2,3]).toEqual(expect.arrayContaining([3, 2])); function (even without having the 1).

  • 1

    @Rafaeltavares well noted. I’ll make some adjustments to the answer later too :)

  • 1

    To future visitors: I have just noticed one more detail that was not mentioned, if you use toHaveLength and arrayContaining to compare [1,2] with [1,1], will not work as expected (the test will pass, [1,1] has the same size and is contained in [1,2]).

1


An alternative would be to install the package jest-Extended, whose goal is to add other "matchers" to existing ones, and use the method expect.toIncludeSameMembers(\[members\]) that checks whether two arrays contain equal values in any order:

test('passa quando as matrizes correspondem em uma ordem diferente', () => {
  expect([1, 2, 3]).toIncludeSameMembers([3, 1, 2]);
  expect([{ foo: 'bar' }, { baz: 'qux' }]).toIncludeSameMembers([{ baz: 'qux' }, { foo: 'bar' }]);
});

Note: The above example is part of the manufacturer’s documentation.

Resulting:

 PASS  ./sum.test.js
  ✓ passa quando as matrizes correspondem em uma ordem diferente (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.131 s, estimated 2 s
Ran all test suites.
Done in 3.50s.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.