Jest: Jest 25 invokes setters during toEqual comparison, causing side effects

Created on 2 Apr 2020  路  11Comments  路  Source: facebook/jest

This is similar to https://github.com/facebook/jest/issues/9531.

It is not safe to assign a bunch of setters in the passed object. They may throw or they may have side effects. This isn't theoretical because we may add setters like this to React.

Example: https://repl.it/repls/HuskyCandidConfiguration

test('setter object comparison', () => {
  expect({
    get a() {
      return {};
    },
    set a(value) {
      throw new Error('noo');
    },
    b: {},
  }).toEqual({ 
    a: {},
   });
});

This used to work fine in 24: https://repl.it/repls/AllMediumorchidDeals

This regression is currently blocking upgrading React to Jest 25.

Regression Help Wanted

All 11 comments

I'm guessing we should just try-catch attempting to override, and if we can't do that just keep going leaving the property as it is? We're just trying to give a nicer diff, if we can't do that we should bail and leave things alone. One downside of that is that then we won't know we failed so can't fix it, but probably worth it. Unless we think this is the only case we haven't covered, which seems optimistic

/cc @WeiAnAn @jeysal @pedrottimark

It鈥檚 not just about catching errors. This setter could perform a side effect. For example

set x(value) {
  renders++;
  _x = value
}

Then merely asserting would change the state of the program.

It鈥檚 not safe to call in the first place.

Right, good point. We should just skip trying to change anything that has a setter then? Would that work?

@gaearon opened up #9757, would love your thoughts on it

Can you give me an overview of why we're mutating user-passed objects in the first place? I read the original PRs but I struggle to understand how this relates to cloning or assertion messages.

I think ultimately there are two constraints:

  • None of user code should run
  • None of user objects should be mutated

Is this a guarantee we can provide?

I guess it's expected that getters would run though. So that's an exception.

We don't mutate the actual object passed in - we make a deep copy and mutate that copy. The reason is that we want to replace the properties that use asymmetric matchers and pass with the value, so that it's not highlighted as failing in the diff view.

It's not an optimal solution, ideally we'd have a smarter object diffing than a full serialization then comparing strings

What if you have a matcher on a property with a setter? How would that work?

I think maybe to fix this, you'd need to turn setters into "regular" properties during cloning instead of cloning setters and then ignoring them.

Ooh, just dropping the setter from the descriptor sounds good. We never want to run them anyways, as we only do this stuff after an assertion has failed and we generate a diff

Pushed that up

Was this page helpful?
0 / 5 - 0 ratings

Related issues

paularmstrong picture paularmstrong  路  3Comments

hramos picture hramos  路  3Comments

StephanBijzitter picture StephanBijzitter  路  3Comments

GrigoryPtashko picture GrigoryPtashko  路  3Comments

samzhang111 picture samzhang111  路  3Comments