Cameron Hudson
Cameron Hudson

Reputation: 3935

How to expect two arrays to be disjoint

I need to assert that an array does not contain any of the elements of a second array. In other words, I need to assert that two arrays are disjoint.

I have tried the following two approaches. To be clear, I expect the following tests to fail.

it('expects arrays to be disjoint', () => {
    expect(['a', 'b', 'c']).not.toEqual(expect.arrayContaining(['c', 'd']));
});

and

it('expects arrays to be disjoint', () => {
    expect(['a', 'b', 'c']).toEqual(expect.not.arrayContaining(['c', 'd']));
});

These tests pass, even though the element 'c' is common to both arrays.

How can I write this test in such a way that the test will fail if the arrays aren't disjoint?

Below is the best that I've been able to do. Is there a more idiomatic way do to it with Jest?

it('expects arrays to be disjoint', () => {
    const intersection = ['a', 'b', 'c'].filter((value) => ['c', 'd'].includes(value));
    expect(intersection.length).toBe(0);
});

Upvotes: 1

Views: 868

Answers (3)

jonrsharpe
jonrsharpe

Reputation: 122091

In a case like this, I'd consider writing a custom matcher, for example:

expect.extend({
  toIntersect(received, expected) {
    const intersection = new Set(received).intersection(new Set(expected));
    return {
      message: () => `${this.utils.matcherHint('toIntersect', 'received', 'expected', { isNot: this.isNot })}\n`
        + '\n'
        + `Intersection: ${this.utils.printReceived(intersection)}\n`
        + `Expected: ${this.utils.printExpected(expected)}\n`
        + `Received: ${this.utils.printReceived(received)}\n`,
      pass: intersection.size > 0,
    };
  }
});

Then you can simply write:

it('expects arrays to be disjoint', () => {
  expect(['a', 'b', 'c']).not.toIntersect(['c', 'd']);
});

and get useful feedback:

    expect(received).not.toIntersect(expected)

    Intersection: Set {"c"}
    Expected: ["c", "d"]
    Received: ["a", "b", "c"]

Note this uses the intersection method, which requires at least Node 22.


The key thing you should be looking for is clear diagnostics when the test fails. Your suggestion gives you this:

    expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: 1

which is not very helpful at all. Even the simple upgrade to:

expect(intersection).toHaveLength(0)

is a lot better:

    expect(received).toHaveLength(expected)

    Expected length: 0
    Received length: 1
    Received array:  ["c"]

Note that this shows you not just that the length was unexpected, but what was actually in the array - the intersecting value "c". (If you're using ESLint's Jest plugin, I'd strongly recommend enabling jest/prefer-to-have-length for exactly this reason).


Similarly this other answer provides:

    expect(received).not.toIncludeAnyMembers(expected)

    Expected list to not include any of the following members:
      ["c", "d"]
    Received:
      ["a", "b", "c"]

Again this lets you see what actually happened.

Upvotes: -3

With the 'jest-extended' package the following would be an even shorter version:

expect(['a', 'b', 'c']).not.toIncludeAnyMembers(['c', 'd']);

Upvotes: 1

skyboyer
skyboyer

Reputation: 23735

Your approach looks fine and idiomatic enough as well as .some() suggested by @jarmod.

There are no native support for such a check for a reason: ordering matters for array(unlike Set recently introduced as part of ECMAScript). So we way more often need to respect order than ignore it.

Since both filter + includes and some run M*N checks, for really long arrays we may convert them both(with M + N operations) into Set or Object(using some property as a key) and then check for intersection by in or Set.has()(that would need min(M, N) operations). So it would be O(N) instead of O(N^2). But in real world we typically mock some short data for unit tests, until it's something integration testing, so it could be probably be better for readability to has check with some since it's shorter, more clear => more readable.

Upvotes: 1

Related Questions