Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow toMatchObject to match recursively on any depth of an object #3326

Closed
wants to merge 1 commit into from
Closed

Allow toMatchObject to match recursively on any depth of an object #3326

wants to merge 1 commit into from

Conversation

kamilogorek
Copy link
Contributor

Summary

Allow the user to recursively match a subset of an object, no matter where it lives in the base object.

For example:

const houseForSale = {
  bath: true,
  bedrooms: 4,
  kitchen: {
    amenities: ['oven', 'stove', 'washer'],
    area: {
      width: 20,
      height: 30,
    },
    wallColor: 'white',
  },
};
const desiredHouse = {
  area: {
    width: 20,
    height: 30,
  }
};

test('the house has my desired features', () => {
  expect(houseForSale).toMatchObject(desiredHouse);
});

Resolves #2506

Test plan

✓ {pass: true} expect({"a": {"b": {"c": "c"}}}).toMatchObject({"b": {"c": "c"}}) (1ms)
✓ {pass: true} expect({"a": {"b": {"c": {"d": "d"}, "e": "e"}}}).toMatchObject({"c": {"d": "d"}}) (2ms)
✓ {pass: false} expect({"a": {"b": {"c": "c"}}}).toMatchObject({"b": {"d": "d"}}) (3ms)

@facebook-github-bot
Copy link
Contributor

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla - and if you have received this in error or have any questions, please drop us a line at [email protected]. Thanks!

If you are contributing on behalf of someone else (eg your employer): the individual CLA is not sufficient - use https://developers.facebook.com/opensource/cla?type=company instead. Contact [email protected] if you have any questions.

@kamilogorek kamilogorek reopened this Apr 19, 2017
@kamilogorek
Copy link
Contributor Author

CLA Signed. Could you kindly revalidate @facebook-github-bot? :shipit:

@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@cpojer
Copy link
Member

cpojer commented Apr 19, 2017

Pheww.. I'm not sure this is a good idea. This seems way too relaxed to me, tbh. Is there any real world example that makes this a good idea?

@kamilogorek
Copy link
Contributor Author

We can ask @ismay or other people from #2506 and related issues :) I just thought that it'll be a good place to start getting familiar with the codebase for me. It indeed seems a little bit too relaxed, but it can be used as an additional feature/extension of an API and not the default behavior of toMatchObject().

@cpojer
Copy link
Member

cpojer commented Apr 19, 2017

Hey @kamilogorek,

I think indeed that this is a bit too much. I appreciate though that you are contributing – if you'd like to pick another task for something that is less controversial, please feel free to join our discord channel (see the help section on the website) and ping me there and we'll find something good :)

@cpojer cpojer closed this Apr 19, 2017
@kamilogorek
Copy link
Contributor Author

@cpojer I definitely will once I get back from my vacations in May! :)

Cheers!

@ecbrodie
Copy link
Contributor

ecbrodie commented Mar 8, 2018

@cpojer I know that this PR was closed a while ago, but you were challenging the author @kamilogorek on a real-life example for when this change may be useful.

I am currently migrating test code from a project that's using Chai/Sinon to purely use Jest instead, both for test expectations and mocking. Some of the tests are using Chai's calledWithMatch() matcher to execute a partial comparison of the expected object to what the Sinon mock was called with, at all levels of the object.

In my particular case, I am testing an object in our API Middleware layer. The return object has a key called meta, that contains various metadata information about the request. In one particular test, I'm asserting that the status code in meta is the correct value. So, with Chai-Sinon, I had this test expectation:

expect(mockObject).calledWithMatch({
  meta: {
    statusCode: 500,
  },
});

When I switch from a Sinon to a Jest mock (via jest.fn(() => ...)) and I switch to Jest's toBeCalledWith() matcher, I change my code to look like so:

expect(mockObject).toBeCalledWith(expect.objectContaining({
  meta: {
    statusCode: 500,
  },
}));

Unfortunately, the test fails because of other keys that are in the meta object. I don't want to have to specify these keys in my test because they are irrelevant to this particular test and are tested elsewhere. Another option that works is to wrap the meta object in my test expectation with another expect.objectContaining() call, but that's starting to make this test pretty verbose. Especially when this is a pattern that I'll have to repeat for multiple tests in my project.

I believe that this additional information is fruitful enough to revive a healthy conversation about this proposed change. I'd love to hear what you think. Thanks a bunch.

@fubhy
Copy link

fubhy commented Mar 11, 2018

I have the exact same use-case that @ecbrodie is mentioning. I am testing fetch() calls with large POST body payloads. Most of these payloads is irrelevant to my individual test cases and I don't want to repeat it in every test. It would be incredibly useful to be able to match subsets.

@peterstarling
Copy link

Is there any progress on this? It would be helpful especially in cases such as the one mentioned by @ecbrodie

@markmsmith
Copy link

I'm interested in support for nested matchers too. My use case is that I'm wanting to check parts of a request object that's being logged, eg:

expect(logger.info).toHaveBeenLastCalledWith(
  expect.objectContaining({
    env: 'production',
    accountId,
    req: expect.objectContaining({
      method: HttpMethod.POST,
      url: ApiService.TPAN_UPLOAD_ROUTE,
      headers: expect.objectContaining({
        "x-amzn-trace-id": "some-alb-generated-trace-id",
        "x-forwarded-proto": "https",
        "x-forwarded-for": "some_client_ip"
      })
    })
  }),
  expectedMessage
);

Without this I need to do dig through the functions mock.args.calls object and then deconstruct the different pieces to test them separately.

@SimenB
Copy link
Member

SimenB commented Jul 15, 2019

Doesn't that work now that we have asymmetric matchers?

@markmsmith
Copy link

Hmm, I tried to create a standalone example to reproduce the problem but it worked, and when I went back and modified my code to use toHaveBeenLastCalledWith() again, that's working now too, so I guess I must have had some other mistake that I fixed in the process.
In my (poor) defense, it's really hard to see what's wrong when there's a mismatch on a large object like a Restify request - it'd be nice if it only showed the mismatched fields when testing for a partial match, rather than all the given fields (but I can understand if others felt differently).
Anyway, sorry for the confusion.

Here's the simple test example that shows it's all good:

it('test nested match', () => {
  const testFn = jest.fn();
  const nestedArg = {
    topLevel: 'value1',
    topLevel2: 'ignored',
    parent: {
      child1: 'value2',
      child2: {
        grandChild1: 'value3',
        grandChild2: 'unimportant'
      },
      child3: 'ignored'
    }
  };
  testFn(nestedArg);
  expect(testFn).toHaveBeenLastCalledWith(
    expect.objectContaining({
      topLevel: 'value1',
      parent: expect.objectContaining({
        child1: 'value2',
        child2: expect.objectContaining({
          grandChild1: 'value3',
          grandChild2: expect.any(String)
        })
      })
    })
  );
});

@SimenB
Copy link
Member

SimenB commented Jul 16, 2019

@pedrottimark thoughts on the feedback above?

EDIT: there is #6184 which might address it?

@pedrottimark
Copy link
Contributor

@markmsmith /cc @ecbrodie @peterstarling @fubhy

it's really hard to see what's wrong when there's a mismatch on a large object like a Restify request - it'd be nice if it only showed the mismatched fields when testing for a partial match, rather than all the given fields

Yes, this is a realistic use case to improve the report when expect.objectContaining fails

If you rewrite as toMatchObject assertion, then it filters differences to be relevant for the subset

Here is a verbose pattern that you can improve by writing a helper function:

it('test nested match', () => {
  const testFn = jest.fn();

  // Do whatever with mock function (for example, receive a request)

  expect(testFn.mock.calls.length).toBe(1); // or whatever
  expect(testFn.mock.calls[0].length).toBe(1); // or whatever
  expect(testFn.mock.calls[0][0]).toMatchObject({
    topLevel: 'value1',
    parent: {
      child1: 'value2',
      child2: {
        grandChild1: 'value3',
        grandChild2: expect.any(String)
      },
    },
  });
});

For more information, see:

@markmsmith
Copy link

Yeah, that's the form I had re-written it to when I was having trouble. I can add a comment to #6184 with this use case in support of improving objectContaining.
Thanks!

@github-actions
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Match object recursively (toMatchObject)
9 participants