4

In this Jasmine test I compare two objects that are almost identical, the only difference is that the second object has an additional undefined member.

describe('Testing', function () {

  it('should compare two objects', function () {


    var obj1 = {a: 1, b: 2 };

    var obj2 = {a: 1, b: 2, c: undefined };

    console.log(JSON.stringify(obj1));
    console.log(JSON.stringify(obj2));

    expect(obj1).toEqual(obj2);


  });

The test fails, however printing the two objects with JSON.stringify result in two identical outputs.

{"a":1,"b":2}
{"a":1,"b":2}

Browsing the objects one can find the difference, however in complex objects this is not that easy. Any suggestions on how to approach this?

ps0604
  • 1,227
  • 23
  • 133
  • 330
  • in javascript ... `({}) != ({})` so, no chance with anything IN the objects – Jaromanda X Feb 16 '17 at 03:19
  • 1
    JSON has no `undefined` value, so anything with that type is ignored by `JSON.stringify`. The two objects *aren't* equal -- one has an extra property -- so naturally Jasmine tells you that. What problem are you finding here? – Ryan Plant Feb 16 '17 at 03:33
  • I copied the exact same code (plus the missing final `});`) into a React project in which testing is performed with `react-scripts test --env=jsdom`. The testing here is with jest, not jasmine, but jest is built at least partially on jasmine. When I run the test, it passes, as I think it should. If I force the test to fail (by adding `.not` into the `expect` statement) just to see the output, I get the following: `Expected value to not equal: {"a": 1, "b": 2, "c": undefined}; Received: {"a": 1, "b": 2}`. – Andrew Willems Feb 16 '17 at 03:35
  • @JaromandaX, you're right about object (in)equality in JavaScript, but the Jasmine `toMatch` method compares object property values and is not the same thing as `==` or `===`. – Andrew Willems Feb 16 '17 at 03:38
  • My above comment about `toMatch` should be about `toEqual`. After correcting for this typo, the comment is still true. To compare objects by reference requires `toBe`, not `toEqual`. – Andrew Willems Feb 16 '17 at 03:46
  • @RyanPlant, the two objects might not be equal by reference, but they _are_ equal by value. `obj2.c` is `undefined` because it is explicitly stated as such, but `obj1.c` is also `undefined` because any property not explicitly defined on an object is `undefined`. Therefore `obj1.c === obj2.c` is true. – Andrew Willems Feb 16 '17 at 03:49
  • @RyanPlant problem is to compare complex objects that are different because there's a buried undefined value – ps0604 Feb 16 '17 at 03:56

2 Answers2

1

Your question is based on two misconceptions:

  1. In obj2 c is a defined property with the value undefined.
  2. stringify() does not serialize undefined according to the spec - your test is not safe

Both objects are unequal.

On Jasmine toEqual()

toEqual uses the internal util.equals() which will compare the object key by key for all enumerable keys defined in a and b.

After some type checks it's getting into comparing the keys of the object

On defining properties

Take a look at the ECMAscript spec. This internal Put method is called when you create object literals:

11.1.5 Object Initialiser

[...]

The production PropertyNameAndValueList : PropertyAssignment is evaluated as follows:

  1. Let obj be the result of creating a new object as if by the expression new Object() where Object is the standard built-in constructor with that name.
  2. Let propId be the result of evaluating PropertyAssignment.
  3. Call the [[DefineOwnProperty]] internal method of obj with arguments propId.name, propId.descriptor, and false.
  4. Return obj.

[...]

The production PropertyAssignment : PropertyName : AssignmentExpression is evaluated as follows:

  1. Let propName be the result of evaluating PropertyName.
  2. Let exprValue be the result of evaluating AssignmentExpression.
  3. Let propValue be GetValue(exprValue).
  4. Let desc be the Property Descriptor{[[Value]]: propValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}

Similary defining properties via member expression:

8.12.5 [[Put]] ( P, V, Throw ) When the [[Put]] internal method of O is called with property P, value V, and Boolean flag Throw, the following steps are taken:

[...]

  1. Else, create a named data property named P on object O as follows

    a. Let newDesc be the Property Descriptor {[[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}.

    b. Call the [[DefineOwnProperty]] internal method of O passing P, newDesc, and Throw as arguments.

The implementation of DefineOwnProperty , which is described in 8.12.9 [[DefineOwnProperty]] (P, Desc, Throw), will not be repeated. Also MDN says that even the default value is undefined.

Check:

> var obj2 = {a: 1, b: 2, c: undefined };
> obj2.hasOwnProperty("c");
< true

On stringify()

Take a look at the ECMAscript spec on stringify() or at the JSON spec:

Possible JSON values

(Source: json.org)

Here's a piece of the ECMAscript spec:

  1. Else a. Let K be an internal List of Strings consisting of the names of all the own properties of value whose [[Enumerable]] attribute is true. The ordering of the Strings should be the same as that used by the Object.keys standard built-in function.

They say, that the enumerated properties of that object should conform to the order (and result) of Object.keys(). Let's test that ...

> var obj2 = {a: 1, b: 2, c: undefined };
> Object.keys(obj2);
< ["a", "b", "c"]

Uh, they're right!

Then there's a Str() function which defines the behavior of handling undefined values. There are a couple of If Type() ... steps, which do not apply for an undefined value, ending with

  1. Return undefined.

When called by an object serializer:

  1. For each element P of K.

    a. Let strP be the result of calling the abstract operation Str with arguments P and value.

    b. If strP is not undefined

    [...]

try-catch-finally
  • 7,436
  • 6
  • 46
  • 67
0

As explained in the comments and in one of the other answers both objects are unequal.

Luckily in Jasmine 2.5 you can solve this using a Custom equality tester (defining your own equals):

function customEquality(a, b) {
    let keys,
        key,
        equal = true;
    // Store unique list of keys over both objects
    keys = Object.keys(a).concat(Object.keys(b)).reduce(function(result, name) {
        if (!result.includes(name)) {
            result.push(name);
        }
        return result;
    }, []);
    for (key of keys) {
        // ignore when keys are defined in both objects, 
        // having the value undefined
        if (typeof a[key] === "undefined" && a.hasOwnProperty(key) &&
            typeof b[key] === "undefined" && b.hasOwnProperty(key)) {
                continue;
        }
        equal = equal && b[key] === a[key];
    }
    return equal;
}

jasmine.addCustomEqualityTester(customEquality);

Insert this in a beforeEach() in the same block as your tests or within the actual it().

This basic tester will ignore undefined values present in both objects. If an undefined value is present in one object it will not be treated as equal.

Note that this tester behaves much different from a pure toEqual() in that it does not compare Arrays or nested objects nor other object types like DOM nodes. It's just an example to direct you.

try-catch-finally
  • 7,436
  • 6
  • 46
  • 67