jest icon indicating copy to clipboard operation
jest copied to clipboard

"Received: serializes to the same string" on object equality checking

Open sabriele opened this issue 6 years ago • 45 comments

🐛 Bug Report

Using .toMatchObject() returns failing test with message Received: serializes to the same string

image

To Reproduce

I am trying to check the users object I receive against my expectedUsers. The received object coming back from MongoDB contains the fields "__v" and "_id" which I do not want to check for (they always change for every test). As such, I am using .toMatchObject() and cannot use something else like .toEqual(). My test snippet is below:

test("should show all existing users", async () => {
  const expectedUsers = [
    {
      email: "[email protected]",
      friends: [],
      followers: [],
      following: [],
      blocked: []
    },
    {
      email: "[email protected]",
      friends: ["[email protected]", "[email protected]"],
      followers: [],
      following: [],
      blocked: []
    },
    {
      email: "[email protected]",
      friends: [],
      followers: [],
      following: [],
      blocked: ["[email protected]"]
    }
  ];
  await request(app)
    .get(route(path))
    .expect("Content-Type", /json/)
    .expect(200);

  const users = await User.find();

  expect(users).toMatchObject(expectedUsers);
});

(request is made with supertest)

Expected behavior

As documented here,

Use .toMatchObject to check that a JavaScript object matches a subset of the properties of an object. It will match received objects with properties that are not in the expected object.

Since the expected objects is a subset of received objects, I expect my test to pass.

npx envinfo --preset jest result

System:
  OS: macOS 10.14.4
  CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
Binaries:
  Node: 10.15.2 - ~/.asdf/shims/node
  npm: 6.9.0 - ~/.asdf/shims/npm
npmPackages:
  jest: ^24.8.0 => 24.8.0 

sabriele avatar May 20 '19 10:05 sabriele

cc @pedrottimark

thymikee avatar May 20 '19 10:05 thymikee

@sabriele Yes, your choice of toMatchObject makes sense. So we can trouble shoot:

  • Can you please paste the output from console.log(users)
  • Does the application use mongoose

pedrottimark avatar May 20 '19 13:05 pedrottimark

@sabriele From reading Jest code and guessing about MongoDB, users array might have non-index properties which toMatchObject should (but does not) ignore.

Here is a work-around to get rid of them:

expect([...users]).toMatchObject(expectedUsers)

If you can paste the received users before work-around, we can make a realistic regression test.

pedrottimark avatar May 24 '19 17:05 pedrottimark

I have the same issue. Here is the test for a react custom hook:

import {renderHook} from 'react-hooks-testing-library'

import useTheme, {DEFAULT_THEME} from 'components/globalStyle/useTheme'

it('should set the global theme', () => {
  const setTheme = () => {}
  const expected = {...DEFAULT_THEME, setTheme}
  const {result} = renderHook(useTheme)

  expect(result.current).toMatchObject(expected)
})

This test returns the following error:

Error: expect(received).toMatchObject(expected)

Expected: {"palette": "dark", "setTheme": [Function setTheme], "textSize": "normal"}
Received: serializes to the same string

I tried the shallow copy trick that @pedrottimark suggested but it didn't work (same error). However, the following seems to work just fine:

import {renderHook} from 'react-hooks-testing-library'

import useTheme, {DEFAULT_THEME} from 'components/globalStyle/useTheme'

it('should set the global theme', () => {
  const Mock = jest.fn()
  const setTheme = new Mock()
  const {result} = renderHook(useTheme)
  const expected = {...DEFAULT_THEME, setTheme}

  expect(result.current).toMatchObject(expected)
})

Setting const setTheme = jest.fn() didn't work 🤷‍♂️

Error: expect(received).toMatchObject(expected)

- Expected
+ Received

  Object {
    "palette": "dark",
-   "setTheme": [Function mockConstructor],
+   "setTheme": [Function setTheme],
    "textSize": "normal",
  }

matchatype avatar May 25 '19 19:05 matchatype

@matchatype If the problem in your https://github.com/facebook/jest/issues/8475#issuecomment-495943549 is like #8166 that deep-equality matchers compare functions according to referential identity, then we recommend asymmetric matcher as expected value, see https://jestjs.io/docs/en/expect#expectanyconstructor

it('should set the global theme', () => {
  const setTheme = expect.any(Function)
  const expected = {...DEFAULT_THEME, setTheme}
  const {result} = renderHook(useTheme)

  expect(result.current).toMatchObject(expected)
})

pedrottimark avatar May 26 '19 15:05 pedrottimark

That does indeed work! However, I'm still confused: all examples should result in the same behavior. Instead, each triggers a completely different response:

  • Received: serializes to the same string;
  • Test passing;
  • Error: expect(received).toMatchObject(expected).

matchatype avatar May 26 '19 16:05 matchatype

The recent change to display serializes to the same string makes more obvious when there are inconsistencies between the comparison in the matcher and the feedback in the report.

@matchatype In the case that you describe:

  • comparison is correct (although unexpected) that () => {} or jest.fn() as expected value are not referentially equal to (that is, not the same instance as) the function returned by the hook
  • report is confusing because unequal values can have the same serialization

Deep-equality matchers compare different instances of functions:

  • like symbols Symbol() is not equal to Symbol()
  • unlike arrays or objects: [0] is equal to [0] or {key: 'value'} is equal to {key: 'value'}

If you think of the returned data structure as a tree, there is a difference between asserting a primitive value as a leaf, and asserting a function or symbol (when the caller does not provide it as an argument).

I am not sure why the work-around that you found solves the problem :)

A long-term goal for Jest is to bridge gaps like this between the comparison and the report.

pedrottimark avatar May 26 '19 16:05 pedrottimark

serializes to the same string is symptom of a different problem in the original https://github.com/facebook/jest/issues/8475#issue-446046819

  1. comparison is incorrect: toMatchObject matcher compares non-index properties (that is, symbols or non-numeric strings) of arrays same as toEqual matcher, instead of being able to ignore properties according to expected subset
  2. getObjectSubset helper ignores non-index properties in received value for report, even if they were in the expected subset
  3. pretty-format package ignores non-index properties, even if getObjectSubset included them

The difficulty to solve those problems: is 2. medium, 1. difficult, 3. breaking

pedrottimark avatar May 26 '19 16:05 pedrottimark

Extremely helpful @pedrottimark Many thanks 🙏Yes, the fact that work-around actually passed totally baffled me.

matchatype avatar May 26 '19 17:05 matchatype

I have the same problem, for me the problem comes from the function I have in the object. The solution for me is to mock function by jest.fn() and put it to input props and expected object.

toEqual in jest can compare two object, it is cool (in js we can't compare directly by '=='), but if the object contains an function (like () => {}), it will have problem to compare.

AlbertWhite avatar Jun 05 '19 11:06 AlbertWhite

Hi @pedrottimark, I apologise for the tardy reply; this was a weekend project and I simply got swamped with work.

Yes, I am using mongoose; I did a diff on the result of console.log(users) and console.log([...users]) and they are exactly the same:

[ { friends: [],
    followers: [],
    following: [],
    blocked: [],
    _id: 5cfbb57e37912c8ff6d2f8b1,
    email: '[email protected]',
    __v: 0 },
  { friends:
      [ '[email protected]', '[email protected]' ],
    followers: [],
    following: [],
    blocked: [],
    _id: 5cfbb57e37912c8ff6d2f8b2,
    email: '[email protected]',
    __v: 0 },
  { friends: [],
    followers: [],
    following: [],
    blocked: [ '[email protected]' ],
    _id: 5cfbb57e37912c8ff6d2f8b3,
    email: '[email protected]',
    __v: 0 } ]

Just like @matchatype I too tried the shallow copy trick but it gave me the same error.

Thank you for trying to help me troubleshoot this! I really appreciate it.

sabriele avatar Jun 08 '19 13:06 sabriele

@sabriele Thank you for the output. When I copy and paste into a local test file, there is syntax error for values of _id properties like 5cfbb57e37912c8ff6d2f8b1 instead of '5cfbb57e37912c8ff6d2f8b1'

That confirms mongoose provides some methods on user object instances.

EDIT: That is, a method that somehow “improved” the default output from console.log

If shallow copy of the array did not help, then the next step is something like:

expect(users.map(user => user.toObject())).toMatchObject(expectedUsers);

See https://mongoosejs.com/docs/api.html#document_Document-toObject

Converts this document into a plain javascript object, ready for storage in MongoDB.

If that is a solution, then I will have some follow-up questions to understand what is the problem.

pedrottimark avatar Jun 10 '19 17:06 pedrottimark

The toObject works for me

patran avatar Jun 21 '19 01:06 patran

@patran So I can understand the problem in toMatchObject if your test gets an array of objects from MongoDB with mongoose, can you add console.log(…) for original array and first object:

  • Object.getOwnPropertyDescriptors(array) copy and then delete array index properties
  • Object.getOwnPropertyDescriptors(array[0]) copy and then delete properties of the data

Paste the results after editing to delete properties that are not added by mongoose. Thank you!

pedrottimark avatar Jun 21 '19 19:06 pedrottimark

Have same problem

js2me avatar Jul 05 '19 14:07 js2me

I have similar problem comparing Buffers. expect(a).toEqual(b) throws "serializes to the same string" expect(a.equals(b)).toBe(true) works fine

I have tried to find any difference between these objects using Object.getOwnPropertyDescriptors, but looks like they are the same.

shrpne avatar Jul 30 '19 13:07 shrpne

I run into the "serializes to the same string" issue when using toMatchObject. The objects had functions defined and was the reason toMatchObject failed. I worked around the issue by mocking them:

const mockFunctions = <T extends Record<string, any>>(obj: T, mock: any): T => {
  const copy = { ...obj };
  Reflect.ownKeys(copy)
    .filter(key => typeof Reflect.get(copy, key) === "function")
    .forEach(key => Reflect.set(copy, key, mock));
  return copy;
};

For toMatchObject to work as expected it was important to use the same jest mock on both objects.

const objectToCompare = (name: string) => {
  const nameAsFunc = (): string => name;

  return {
    name,
    nameAsFunc
  };
};

describe("toMatchObject tests", () => {
  it("can compare objects with functions", () => {
    const mock = jest.fn();

    const first = objectToCompare("name");
    const second = objectToCompare("name");

    // Gives "serializes to the same string"
    expect(first).toMatchObject(second);

    // Works
    expect(mockFunctions(first, mock)).toMatchObject(mockFunctions(second, mock));
  });
});

jaspenlind avatar Aug 26 '19 00:08 jaspenlind

@matchatype If the problem in your #8475 (comment) is like #8166 that deep-equality matchers compare functions according to referential identity, then we recommend asymmetric matcher as expected value, see https://jestjs.io/docs/en/expect#expectanyconstructor

it('should set the global theme', () => {
  const setTheme = expect.any(Function)
  const expected = {...DEFAULT_THEME, setTheme}
  const {result} = renderHook(useTheme)

  expect(result.current).toMatchObject(expected)
})

That worked for me too. Thanks !

EduardoFLima avatar Sep 27 '19 12:09 EduardoFLima

I'm also experiencing this issue. This is my workaround:

expect(JSON.stringify(result.current)).toEqual(JSON.stringify(expected));

manhhailua avatar Oct 03 '19 07:10 manhhailua

@manhhailua Thank you so much! This worked for me after hours of agony.

I'm also experiencing this issue. This is my workaround:

expect(JSON.stringify(result.current)).toEqual(JSON.stringify(expected));

alexanderela avatar Nov 22 '19 22:11 alexanderela

@pedrottimark Are you guys planning to fix this any time soon? I am also using shallow rendering and experience bad test results. 😕

Here is my test code:

expect(shallowResult.props.children).toEqual(
            [<Todo todo={fakeTodosData.data[0]} />,
            <Todo todo={fakeTodosData.data[1]} />]
        );

When shallowResult.props.children is the correct thing my test outs this:

  Expected: [<Todo todo={{"description": "", "id": 100, "title": "Text!"}} />, <Todo todo={{"description": "More text...", "id": 42, "title": "Other Text"}} />]
    Received: serializes to the same string

^ (horrible output and really should be changed)

When I change the matcher to "toContainEqual" is outputs this:

  Expected value: [<Todo todo={{"description": "", "id": 100, "title": "Text!"}} />, <Todo todo={{"description": "More text...", "id": 42, "title": "Other Text"}} />]
    Received array: [<Todo todo={{"description": "", "id": 100, "title": "Text!"}} />, <Todo todo={{"description": "More text...", "id": 42, "title": "Other Text"}} />]

(^ a failing test showing that the results are exactly the same. This is super confusing and it also should really be changed)

Even using the "stringify-all-the-things" hack from @manhhailua does not work for me. It seems that the "key" field that is necessary when rendering components in a loop is hidden away in the test output. Here is my stringified test failure:

    Expected: "[{\"key\":null,\"ref\":null,\"props\":{\"todo\":{\"id\":100,\"title\":\"Text!\",\"description\":\"\"}},\"_owner\":null,\"_store\":{}},{\"key\":null,\"ref\":null,\"props\":{\"todo\":{\"id\":42,\"title\":\"Other Text\",\"description\":\"More text...\"}},\"_owner\":null,\"_store\":{}}]"
    Received: "[{\"key\":\"key0\",\"ref\":null,\"props\":{\"todo\":{\"id\":100,\"title\":\"Text!\",\"description\":\"\"}},\"_owner\":null,\"_store\":{}},{\"key\":\"key1\",\"ref\":null,\"props\":{\"todo\":{\"id\":42,\"title\":\"Other Text\",\"description\":\"More text...\"}},\"_owner\":null,\"_store\":{}}]"

@pedrottimark Are you the maintainer of this 'react-test-renderer/shallow' project? I would very much like this to be fixed, and I have bandwidth to work on this right now if you need help. This is extremely disappointing to me as I do very much like the way 'react-test-renderer/shallow' works (much nicer than enzyme imo). It would be even nicer though if it gave more insight into why the tests are not passing! 😄

Thanks!

JimLynchCodes avatar Dec 29 '19 18:12 JimLynchCodes

In the end my test is passing with this (I was forgetting the "key" field and wasn't aware it was missing until doing the stringified comparison):

 expect(JSON.stringify(shallowResult.props.children)).toEqual(
            JSON.stringify(
                [<Todo todo={fakeTodosData.data[0]} key={'key0'}/>,
                <Todo todo={fakeTodosData.data[1]} key={'key1'}/>]
            )
        );

JimLynchCodes avatar Dec 29 '19 18:12 JimLynchCodes

fyi, swapping .toBe to .toEqual helped my case:)

kiprasmel avatar Jan 29 '20 19:01 kiprasmel

Circular-structured JSONs proof:

// Workaround when Using .toMatchObject() returns failing test with message Received: serializes to the same string
// https://github.com/facebook/jest/issues/8475
const getCircularReplacer = () => {
    const seen = new WeakSet();
    return (key, value) => {
        if (typeof value === 'object' && value !== null) {
            if (seen.has(value)) {
                return;
            }
            seen.add(value);
        }
        return value;
    };
};

const isStringifiedComparisonEqual = (a: Object, b: Object): boolean =>
    JSON.stringify(a, getCircularReplacer()) === JSON.stringify(b, getCircularReplacer());



expect(isStringifiedComparisonEqual(objectA, objectB));

jaimeagudo avatar Feb 12 '20 09:02 jaimeagudo

In my case I had:

const obj = { value: 'value', array: ['array'] };
expect(obj).toMatchObject({ ... });

And got the error, but was able to resolve that, by wrapping nested array with expect.arrayContaining(['array']) (inside toMatchObject). It is because Jest probably doesn't resolve nested array automatically in that case.

soanvig avatar Feb 19 '20 21:02 soanvig

jumping onto this thread, when an object contains methods I run into this:

      const a = {
        getSomething: () => ({
          getSomethingElse: () => ({
            something: 'I want',
          }),
        }),
      };

      const b = {
        getSomething: () => ({
          getSomethingElse: () => ({
            something: 'I want',
          }),
        }),
      };

      expect(a).toMatchObject(b);  //    _Expected: {"getSomething": [Function getSomething]} Received: serializes to the same string_

bbbryan14 avatar Mar 04 '20 22:03 bbbryan14

Hello. Sorry if I missed some message that was describing the issue already, but I've created a sandbox with reproduction for you:

https://codesandbox.io/s/nameless-violet-vk4gn

See the src/index.test.js source and "Tests" tab for the results

RusinovAnton avatar Mar 31 '20 17:03 RusinovAnton

@pedrottimark

Here is a work-around to get rid of [non-index properties]:

expect([...users]).toMatchObject(expectedUsers)

users.slice(0) also gets rid of non-index properties. Might it be faster?

dandv avatar Apr 02 '20 11:04 dandv

users.slice(0) also gets rid of non-index properties. Might it be faster?

The difference is very minor https://jsperf.com/slice-vs-spread-2

jeysal avatar Apr 02 '20 11:04 jeysal

Is there a way to disable "serializes to the same string" so it could resolve positively? Maybe additional configuration for Jest? Quite annoying that we have to look for a workaround every time we need to compare deep nested objects, object methods, etc.

DnEgorWeb avatar Apr 25 '20 08:04 DnEgorWeb