Ava: .deepEqual() compares constructors

Created on 2 Aug 2017  Â·  10Comments  Â·  Source: avajs/ava

There appears to have been discussion on this in the past, but this is preventing me from using assertions in any meaningful way.

The following code fails.

import test from 'ava';

class Foo {
}

test('sameness', t => {
    const f = new Foo();
    f.foo = 10;
    t.deepEqual(f, {foo: 10});
});

Error:

  case › sameness
  /src/bootstrap/test/case.js:9

   8:     f.foo = 10;
   9:     t.deepEqual(f, {foo: 10});         <<
   10: });

  Difference:

  - Foo {
  -   foo: 10,
  - }
  + {
  +   foo: 10,
  + }

Kind of a useless test while checking constructors. For classes that have side effects in constructors, this makes deep-equalling instances of classes almost impossible without adversely affecting the environment.

enhancement question assertions

Most helpful comment

We're close to landing a t.like() assertion which would solve this use case.

All 10 comments

This is also the behavior of the built-in assert:

const assert = require('assert');

class Foo {}

it('foo', () => {
    const f = new Foo();
    f.foo = 10;
    assert.deepStrictEqual(f, {foo: 10});
});
  1) sd

  0 passing (10ms)
  1 failing

  1)  foo:

      AssertionError [ERR_ASSERTION]: Foo { foo: 10 } deepStrictEqual { foo: 10 }
      + expected - actual

(With Mocha)

What you seem to want is something like t.deepLooseEqual() and that we don't currently support.

What you seem to want is something like t.deepLooseEqual() and that we don't currently support.

There might be an approach here that could work but at a cost.

t.deepEqual() is built on top of concordance, which compares enumerable properties, list and iterator items. It also looks at constructors, string tags (Object.prototype.toString.call(foo)) and has value-specific behavior, e.g. comparing Arguments to an array, or comparing non-enumerable Error names.

If we strip the comparison down to just enumerable properties and list and iterator items, it just might work for your use case @Qix-. However this comes at a cost. Diffs will not be as optimized as they are with the regular deepEqual. Extraneous or missing Map entries won't be detected at all, since we'd be comparing [key, value] arrays instead. Errors and typed arrays may compare in unexpected ways.

Maybe this is worth it, or maybe we should advocate an approach where the actual value is stripped off its constructors and whatnot so it can be compared against object literals.

@novemberborn What do you think about having a loose structural matcher? Like node-tap's t.match(): http://www.node-tap.org/asserts/

@sindresorhus ah yes, that could be another take. Would you say that it's only loose when it comes to enumerable properties (and constructors / string tags)? E.g. arrays still need to have the same number of items, that each need to (loosely) match.

Does match detect _extra_ properties?

What I need is a way to check that an instance of a class strict-deep-equals an object sans constructor, since many of the constructors in this particular project have side effects. Plus, to be able to show object literals, I no longer have to guess the structure of the object (and I don't need to worry about encapsulation in these cases, so it's fine to assert the inner structure of these objects).

Doesn't seem like all that uncommon of a use-case.

Does match detect extra properties?

No

Hi folks!
t.match could be a very useful feature.
Is it ok for you to use tapjs/tmatch to implement it? If so I can open a pr :)

Thanks!

@delvedor I haven't looked at tmatch too closely, but we'd need to understand how it differs with our t.deepEqual implementation when it comes to:

  • comparing primitives and boxed values
  • comparing negative zero
  • lists with different lengths
  • iterables with a different number of items
  • comparing constructor values and string tags

I solved this with a JSON flattener; it may not work for everyone:

Before:

- OptionsSync {
-   defaults: {},
-   storageName: 'options',
- }
+ {
+   defaults: {},
+   storageName: 'options',
+ }

Solution:

function flattenInstance(object) {
    return JSON.parse(JSON.stringify(object));
}

test('basic usage', t => {
    t.deepEqual(flattenInstance(new OptionsSync()), {
        defaults: {},
        storageName: 'options'
    });
});

We're close to landing a t.like() assertion which would solve this use case.

Was this page helpful?
0 / 5 - 0 ratings