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

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

Open
sabriele opened this issue May 20, 2019 · 43 comments
Open

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

sabriele opened this issue May 20, 2019 · 43 comments

Comments

@sabriele
Copy link

🐛 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: "andy@example.com",
      friends: [],
      followers: [],
      following: [],
      blocked: []
    },
    {
      email: "john@example.com",
      friends: ["andy@example.com", "mary@example.com"],
      followers: [],
      following: [],
      blocked: []
    },
    {
      email: "mary@example.com",
      friends: [],
      followers: [],
      following: [],
      blocked: ["john@example.com"]
    }
  ];
  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 
@thymikee
Copy link
Collaborator

cc @pedrottimark

@pedrottimark
Copy link
Contributor

pedrottimark commented May 20, 2019

@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
Copy link
Contributor

@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.

@matchatype
Copy link

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",
  }

@pedrottimark
Copy link
Contributor

@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)
})

@matchatype
Copy link

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).

@pedrottimark
Copy link
Contributor

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
Copy link
Contributor

serializes to the same string is symptom of a different problem in the original #8475 (comment)

  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

@matchatype
Copy link

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

@AlbertWhite
Copy link

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.

@sabriele
Copy link
Author

sabriele commented Jun 8, 2019

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: 'andy@example.com',
    __v: 0 },
  { friends:
      [ 'andy@example.com', 'mary@example.com' ],
    followers: [],
    following: [],
    blocked: [],
    _id: 5cfbb57e37912c8ff6d2f8b2,
    email: 'john@example.com',
    __v: 0 },
  { friends: [],
    followers: [],
    following: [],
    blocked: [ 'john@example.com' ],
    _id: 5cfbb57e37912c8ff6d2f8b3,
    email: 'mary@example.com',
    __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.

@pedrottimark
Copy link
Contributor

pedrottimark commented Jun 10, 2019

@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.

@patran
Copy link

patran commented Jun 21, 2019

The toObject works for me

@pedrottimark
Copy link
Contributor

@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!

@js2me
Copy link

js2me commented Jul 5, 2019

Have same problem

@shrpne
Copy link

shrpne commented Jul 30, 2019

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.

@jaspenlind
Copy link

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));
  });
});

@EduardoFLima
Copy link

@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 !

@manhhailua
Copy link

manhhailua commented Oct 3, 2019

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

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

@alexanderela
Copy link

@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));

@JimLynchCodes
Copy link

JimLynchCodes commented Dec 29, 2019

@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
Copy link

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'}/>]
            )
        );

@kiprasmel
Copy link

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

@jaimeagudo
Copy link

jaimeagudo commented Feb 12, 2020

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));

@soanvig
Copy link

soanvig commented Feb 19, 2020

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.

@bbbryan14
Copy link

bbbryan14 commented Mar 4, 2020

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_

@RusinovAnton
Copy link

RusinovAnton commented Mar 31, 2020

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

@dandv
Copy link
Contributor

dandv commented Apr 2, 2020

@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?

@jeysal
Copy link
Contributor

jeysal commented Apr 2, 2020

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

@DnEgorWeb
Copy link

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.

@vNNi
Copy link

vNNi commented May 15, 2020

thx @manhhailua

@danny-does-stuff
Copy link

@DnEgorWeb to achieve this functionality you could serialize the objects yourself and compare the results. In my use case this behavior is a good thing because I need to make sure the objects are actually the same all the way through

@zaydek
Copy link

zaydek commented Jul 10, 2020

I had this problem too but I found I could wrap an expect inside of an expect and catch the throw error:

	expect(() => {
		expect(toReact(children)).toEqual([
			"Hello, ",
			<A href="https://google.com">
				world
			</A>,
			"!",
		])
	}).toThrow("serializes to the same string")

I hope this helps someone. By the way you can actually test the throw message using regex: https://jestjs.io/docs/en/expect#tothrowerror.

@johnnybenson
Copy link

johnnybenson commented Nov 12, 2020

Maybe this will help somebody else. I had this error after introducing a circular dependency while writing tests. Removing the circular dependency resolved the issue.

@soullivaneuh
Copy link

Got the same issue with this assert:

expect(error.errors).toMatchObject(expectedErrors);
expect(Object.keys(error.errors)).toBe(Object.keys(expectedErrors));

The goal is to ensure the errors numbers are equal because toMatchObject will not ensure that.

I may compare array length, but the information is restricted to a simple number instead the error key diff.

I finally found a workaround using jest-extended with the toContainAllKeys method:

expect(error.errors).toContainAllKeys(Object.keys(expectedErrors));

However, having a strict-less built-in object comparison method would be a nice addition.

@jpbochi
Copy link

jpbochi commented May 5, 2021

This was my solution:

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

@ShuttleSantalex
Copy link

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

This was my catch! TY

@nzcodarnoc
Copy link

Here's how I solved it. As I understand, in my case I was having a problem matching function names, because the matcher operates on the function identity, and not the name of the function. My solution was to mock the module when the function resided before running the test, the mocking ensured that all the functions have the same identity.

Before (causing the test to fail with "Received: serializes to the same string" on object equality checking")

import { TheInputFormControl } from '../../components/formControls';
...
  it('integrates successfully', () => {
    const result = getField(0);
    expect(result).toMatchObject([
      {
        Component: TheInputFormControl,
        id: 'user_id',
...

After (tests passing)

import { TheInputFormControl } from '../../components/formControls';
...
  jest.mock('../../components/formControls'); // <- this is the secret sauce
  it('integrates successfully', () => {
    const result = getField(0);
    expect(result).toMatchObject([
      {
        Component: TheInputFormControl,
        id: 'user_id',
...

@rachOS
Copy link

rachOS commented Dec 29, 2021

Hello,
Many of yours answer doesn't work for me... But that is my working test:

import {mocked} from "ts-jest/utils" // depreciated in Jest ^28.*
import {CKTable} from "./CKTable"

jest.mock('../CKTable')
const mockedCKtable = mocked(CKTable, true)

...

const expectedOptions = {
        date: new Date(),
        id: 0,
       // etc ...
      };

const mockEntries = Object.entries(mockedCKTable.mock.calls[0][0]);

const expectedEntries = Object.entries(expectedOptions);

expect(JSON.stringify(mockEntries)).toEqual(JSON.stringify(expectedEntries));

@Fewwy

This comment was marked as outdated.

@julian-kingman-lark
Copy link

I have two nearly identical components that I'm testing (via react-native-testing-library), and oddly I get this error for one, but not the other, comparing SVG components. I don't have much else useful to contribute, I'd delve deeper if I knew how to debug further.

Copy link

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label Apr 24, 2024
@Bessonov
Copy link

Kill the stale bot :/

@github-actions github-actions bot removed the Stale label Apr 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests