69

It will be the best explain in on example

expected(someNumber).toBe(1).or.toBe(-2).or.toBe(22) // expect result is 1 or -2 or 22

This is bad syntax, but can do sth like that in jest?

kspacja
  • 4,648
  • 11
  • 38
  • 41
  • What do you mean by _"bad syntax"_? Also, why are you expecting three different results? – evolutionxbox Jun 20 '17 at 13:20
  • This is imaginary syntax -- how it could be, but it isn't :) I wrote it only to show my thoughts – kspacja Jun 20 '17 at 13:23
  • If this isn't possible with jest, then consider changing your logic. Don't test for three different outputs. – evolutionxbox Jun 20 '17 at 13:23
  • I would like to writer sth like regression test. One lib give me a result, but I don't know in advance what it will be based on input. Only what I wanna check is that if result is one of options in array – kspacja Jun 20 '17 at 13:43
  • Can you event unit test a function like that? – evolutionxbox Jun 20 '17 at 13:45
  • Could you elaborate how you can get this different results in the first place. This sounds really uncommon so maybe the problem is more in your code then in a missing feature in Jest. – Andreas Köberle Jun 20 '17 at 14:35
  • I think this is a perfectly good thing to want. I have a promise that I want to ensure has resolved, but the test in question doesn't care what it resolved to. Right now it's undefined, but later it might be changed to something meaningful, and I don't want to have to change my test if that happens. – Mark McKenna Aug 09 '19 at 19:41
  • This is crazy to me how Jest hasn't implemented this yet. Someone ought to make a formal feature request for it. I have an API that could return 2 different strings, and want to expect the response to be either one of the two, not just one. – Josh Yolles Feb 04 '21 at 20:32

6 Answers6

31

A simple way around this is to use the standard .toContain() matcher (https://jestjs.io/docs/en/expect#tocontainitem) and reverse the expect statement:

expect([1, -2, 22]).toContain(someNumber);
Ryan Beard
  • 327
  • 3
  • 2
26

If you really needed to do exactly that, I suppose you could put the logical comparisons inside the expect call, e.g.

expect(someNumber === 1 || someNumber === -2 || someNumber === 22).toBeTruthy();

If this is just for a "quick and dirty" check, this might suffice.

However, as suggested by several comments under your question, there seem to be several "code smells" that make both your initial problem as well as the above solution seem like an inappropriate way of conducting a test.

First, in terms of my proposed solution, that use of toBeTruthy is a corruption of the way Jasmine/Jest matchers are meant to be used. It's a bit like using expect(someNumber === 42).toBeTruthy(); instead of expect(someNumber).toBe(42). The structure of Jest/Jasmine tests is to provide the actual value in the expect call (i.e. expect(actualValue)) and the expected value in the matcher (e.g. toBe(expectedValue) or toBeTruthy() where expectedValue and true are the expected values respectively). In the case above, the actual value is (inappropriately) provided in the expect call, with the toBeTruthy matcher simply verifying this fact.

It might be that you need to separate your tests. For example, perhaps you have a function (e.g. called yourFunction) that you are testing that provides (at least) 3 different possible discrete outputs. I would presume that the value of the output depends on the value of the input. If that is the case, you should probably test all input/output combinations separately, e.g.

it('should return 1 for "input A" ', () => {
  const someNumber = yourFunction("input A");
  expect(someNumber).toBe(1);
});

it('should return -2 for "input B" ', () => {
  const someNumber = yourFunction("input B");
  expect(someNumber).toBe(-2);
});

it('should return 22 for "input C" ', () => {
  const someNumber = yourFunction("input C");
  expect(someNumber).toBe(22);
});

..or at least...

it('should return the appropriate values for the appropriate input ', () => {
  let someNumber;
  someNumber = yourFunction("input A");
  expect(someNumber).toBe(1);

  someNumber = yourFunction("input B");
  expect(someNumber).toBe(-2);

  someNumber = yourFunction("input C");
  expect(someNumber).toBe(22);
});

One of the positive consequences of doing this is that, if your code changes in the future such that, e.g. one (but only one) of the conditions changes (in terms of either input or output), you only need to update one of three simpler tests instead of the single more complicated aggregate test. Additionally, with the tests separated this way, a failing test will more quickly tell you exactly where the problem is, e.g. with "input A", "input B", or "input C".

Alternatively, you may need to actually refactor yourFunction, i.e. the code-under-test itself. Do you really want to have a particular function in your code returning three separate discrete values depending on different input? Perhaps so, but I would examine the code separately to see if it needs to be re-written. It's hard to comment on this further without knowing more details about yourFunction.

Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
  • 3
    To be fair, there are several situations where it's valid for a function under test to return one of several values---e.g. a findShortestPath() in some graph where there are multiple short paths of the same length. – wgoodall01 Mar 12 '19 at 19:50
  • found this method helpful when feature flagging formatting / masking text – devonj Nov 19 '20 at 18:05
  • I just ran into such a situation when my datetime related tests failed because DST started yesterday, and `01:00` just became `+02:00`... – RSeidelsohn Mar 29 '21 at 07:40
23

To avoid putting all the logical comparisons in one statement and using toBeTruthy(), you can use nested try/catch statements:

try {
  expect(someNumber).toBe(1)
}
catch{
  try {
    expect(someNumber).toBe(-2)
  }
  catch{
    expect(someNumber).toBe(22)
  }
}

To make it more convenient and more readable, you can put this into a helper function:

function expect_or(...tests) {
  if (!tests || !Array.isArray(tests)) return;
  try {
    tests.shift()?.();
  } catch (e) {
    if (tests.length) expect_or(...tests);
    else throw e;
  }
}

NB: With Typescript replace line 1 with function expect_or(...tests: (() => void)[]) { to add types to the function parameter.

and use it like this:

 expect_or(
   () => expect(someNumber).toBe(1),
   () => expect(someNumber).toBe(-2),
   () => expect(someNumber).toBe(22)
 );
Davey
  • 2,355
  • 1
  • 17
  • 18
Elmar Schrutek
  • 279
  • 2
  • 3
  • 1
    This approach does not seem to work for `.toMatchSnapshot()` assertion in the try.. – Norfeldt Aug 17 '21 at 13:55
  • Thank you for pointing this out. It seems that .toMatchSnapshot() does not throw an exception when the match fails - that's why it does not work with my solution. You can change this behavior as described here: https://jestjs.io/docs/expect#bail-out, but I would be cautious, as this could clash with the way jest-snapshots work. – Elmar Schrutek Aug 18 '21 at 14:59
8

As @JrGiant suggested, there could be a toBeOneOf, however, it is easy top implement your own matcher:

Example in TypeScript:

expect.extend({
  toBeOneOf(received: any, items: Array<any>) {
    const pass = items.includes(received);
    const message = () =>
      `expected ${received} to be contained in array [${items}]`;
    if (pass) {
      return {
        message,
        pass: true
      };
    }
    return {
      message,
      pass: false
    };
  }
});

// Declare that jest contains toBeOneOf
// If you are not using TypeScript, remove this "declare global" altogether
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeOneOf(items: Array<any>): CustomMatcherResult;
    }
  }
}

describe("arrays", () => {
  describe("getRandomItemFromArray", () => {
    it("should return one of the expected - 1", () => {
      expect(getRandomItemFromArray([1, 2])).toBeOneOf([1, 2])
    });
  });
});

Andre Pena
  • 56,650
  • 48
  • 196
  • 243
4

I recommend using the .toContain(item) matcher. The documentation can be found here.

The below code should work well:

expect([1, -2, 22]).toContain(someNumber);
3

I was also looking for a solution for the expect.oneOf issue. You may want to checkout d4nyll's solution.

Here is an example of how it could work.

expect(myfunction()).toBeOneOf([1, -2, 22]);
Joshua Rose
  • 370
  • 2
  • 10