Jest: expect object not to have property

Viewed 1337

I want to write a test that asserts a given object does not have certain properties.

Say I have a function

function removeFooAndBar(input) {
  delete input.foo;
  delete input.bar;
  return input;
}

Now I want to write a test:

describe('removeFooAndBar', () => {
  it('removes properties `foo` and `bar`', () => {
    const data = {
      foo: 'Foo',
      bar: 'Bar',
      baz: 'Baz',
    };
    expect(removeFooAndBar(data))
      .toEqual(expect.objectContaining({
        baz: 'Baz', // what's left
        foo: expect.not.exists() // pseudo
        bar: undefined // this doesn't work, and not what I want
      }));
  });
});

What's the proper way to assert this?

6 Answers

can you check the result? example?

const result = removeFooAndBar(data)
expect(result.foo).toBeUndefined()
expect(result.bar).toBeUndefined()

you can check initially that the properties were there.

The other option is to extend the expect function: https://jestjs.io/docs/expect#expectextendmatchers

expect.extend({
  withUndefinedKeys(received, keys) {
    const pass = keys.every((k) => typeof received[k] === 'undefined')
      if (pass) {
        return {
          pass: true,
       }
    }
    return {
       message: () => `expected all keys ${keys} to not be defined in ${received}`,
       pass: false,
    }
  },
})
expect({ baz: 'Baz' }).withUndefinedKeys(['bar', 'foo'])

Update after the discussion in the comments

You can use expect.not.objectContaining(). This approach works fine but has one unfortunate edge case: It matches when the property exists, but is undefined or null. To fix this you can explicitly add those values to be included in the check. You need the jest-extended package for the toBeOneOf() matcher.

expect({foo: undefined}).toEqual(expect.not.objectContaining(
    {foo: expect.toBeOneOf([expect.anything(), undefined, null])}
));

An example with nested props that fails:

const reallyAnything = expect.toBeOneOf([expect.anything(), undefined, null]);

expect({foo: undefined, bar: {baz: undefined}}).toEqual(
    expect.not.objectContaining(
        {
            foo: reallyAnything,
            bar: {baz: reallyAnything},
        }
    )
);

Original answer

What I'd do is to explicitly check whether the object has a property named bar or foo.

delete data.foo;
delete data.bar;
delete data.nested.property; 

expect(data).not.toHaveProperty('bar');
expect(data).not.toHaveProperty('foo');
expect(data.nested).not.toHaveProperty('property');
// or
expect(data).not.toHaveProperty('nested.property');

Or make this less repeating by looping over the properties that will be removed.

const toBeRemoved = ['foo', 'bar'];

toBeRemoved.forEach((prop) => {
    delete data[prop];
    expect(data).not.toHaveProperty(prop);
});

However, the loop approach isn't too great for possible nested objects. I believe what you are looking for is expect.not.objectContaining()

expect(data).toEqual(expect.not.objectContaining({foo: 'Foo', bar: 'Bar'}));

expect.not.objectContaining(object) matches any received object that does not recursively match the expected properties. That is, the expected object is not a subset of the received object. Therefore, it matches a received object which contains properties that are not in the expected object. - Jest Documentation

I would just try:

expect(removeFooAndBar(data))
    .toEqual({
        baz: 'Baz'
    })

I'd just try because you know the data value to use it:

const data = {...};
const removed = {...data};
delete removed.foo;
delete removed.bar;
expect(removeFooAndBar(data)).toEqual(removed);

Edit 1: Because of Jest's expect.not, try something like:

const removed = removeFooAndBar(data);
expect(removed).not.toHaveProperty('foo');
expect(removed).not.toHaveProperty('bar');
expect(removed).toHaveProperty('baz');

Do not check object.foo === undefined as others suggest. This will result to true if the object has the property foo set to undefined

eg.

const object = {
  foo: undefined
}

Have you tried use the hasOwnProperty function?

this will give you the following results

const object = {foo: ''};
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(true);

object.foo = undefined;
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(true);

delete object.foo;
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(false);

This answer is a paraphrase of the accepted answer. It is added only because of this exact suggestion to the accepted answer was rejected.

You can explicitly check whether the object has a property named bar or foo.

delete data.foo;
delete data.bar;

expect(data).not.toHaveProperty('bar');
expect(data).not.toHaveProperty('foo');

For nested properties:

delete data.nested.property; 

expect(data.nested).not.toHaveProperty('property');
// or
expect(data).not.toHaveProperty('nested.property');

Or make this less repeating by looping over the properties that will be removed.

const toBeRemoved = ['foo', 'bar', 'nested.property'];

toBeRemoved.forEach((prop) => {
    expect(data).not.toHaveProperty(prop);
});

However, the loop approach isn't too great for possible nested objects. What you are looking for is expect.not.objectContaining().

expect({baz: 'some value'}).toEqual(expect.not.objectContaining(
    {foo: expect.anything()}
));

This approach works fine but has one unfortunate edge case: It matches when the property exists, but is undefined or null:

expect({foo: undefined}).toEqual(expect.not.objectContaining(
    {foo: expect.anything()}
));

would also match. To fix this you can explicitly add those values to be included in the check. You need the jest-extended package for the toBeOneOf() matcher.

expect({foo: undefined}).toEqual(expect.not.objectContaining(
    {foo: expect.toBeOneOf([expect.anything(), undefined, null])}
));

An example with nested props that, expectedly, fails:

const reallyAnything = expect.toBeOneOf([expect.anything(), undefined, null]);

expect({foo: undefined, bar: {baz: undefined}}).toEqual(
    expect.not.objectContaining(
        {
            foo: reallyAnything,
            bar: {baz: reallyAnything},
        }
    )
);
Related