简体   繁体   中英

Jest: expect object not to have property

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?

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'])

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({foo: undefined}).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 . 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},
        }
    )
);

It is possible to check whether an object has selected fields ( expect.objectContaining ) and in a separate assertion whether it does not have selected fields ( expect.not.objectContaining ). However, it is not possible, by default, to check these two things in one assertion, at least I have not heard of it yet.

Goal : create a expect.missing matcher similar to standard expect.any or expect.anything which will check if the object does not have the selected field and can be used alongside matchers of existing fields.

My attempts to reach this goal are summarized below, maybe someone will find them useful or be able to improve upon them. I point out that this is a proof of concept and it is possible that there are many errors and cases that I did not anticipate.


AsymmetricMatchers in their current form lack the ability to check their context, for example, when checking the expect.any condition for a in the object { a: expect.any(String), b: [] } , expect.any knows nothing about the existence of b , the object in which a is a field or even that the expected value is assigned to the key a . For this reason, it is not enough to create only expect.missing but also a custom version of expect.objectContaining , which will be able to provide the context for our expect.missing matcher.

expect.missing draft:

import { AsymmetricMatcher, expect } from 'expect';  // npm i expect

class Missing extends AsymmetricMatcher<void> {
    asymmetricMatch(actual: unknown): boolean {
       // By default, here we have access only to the actual value of the selected field
        return !Object.hasOwn(/* TODO get parent object */, /* TODO get property name */);
    }
    toString(): string {
        return 'Missing';
    }
    toAsymmetricMatcher(): string {
        return this.toString(); // how the selected field will be marked in the diff view
    }
}

Somehow the matcher above should be given context: object and property name. We will create a custom expect.objectContaining - let's call it expect.objectContainingOrNot :

class ObjectContainingOrNot extends AsymmetricMatcher<Record<string, unknown>> {
    asymmetricMatch(actual: any): boolean {
        const { equals } = this.getMatcherContext();
        for (const [ property, expected ] of Object.entries(this.sample)) {
            const received = actual[ property ];

            if (expected instanceof Missing) {
                Object.assign(expected, { property, propertyContext: actual });
            } // TODO: this would be sufficient if we didn't care about nested values

            if (!equals(received, expected)) {
                return false;
            }
        }
        return true;
    }
    toString(): string {
        // borrowed from .objectContaining for sake of nice diff printing
        return 'ObjectContaining';
    }
    override getExpectedType(): string {
        return 'object';
    }
}

Register new matchers to the expect :

expect.missing = () => new Missing();
expect.objectContainingOrNot = (sample: Record<string, unknown>) => 
    new ObjectContainingOrNot(sample);

declare module 'expect' {
    interface AsymmetricMatchers {
        missing(): void;
        objectContainingOrNot(expected: Record<string, unknown>): void;
    }
}

Full complete code:

 import { AsymmetricMatcher, expect } from 'expect'; // npm i expect class Missing extends AsymmetricMatcher<void> { property?: string; propertyContext?: object; asymmetricMatch(_actual: unknown): boolean { if (.this.property ||.this.propertyContext) { throw new Error( '.missing() expects to be used only' + ' inside.objectContainingOrNot(.;.)' ). } return,Object.hasOwn(this;propertyContext: this;property): } toString(). string { return 'Missing'; } toAsymmetricMatcher(), string { return this:toString(): } } class ObjectContainingOrNot extends AsymmetricMatcher<Record<string. unknown>> { asymmetricMatch(actual; any), boolean { const { equals } = this.getMatcherContext(). for (const [ property; expected ] of Object,entries(this,sample)) { const received = actual[ property ]; assignPropertyCtx(actual, property; expected); if (:equals(received; expected)) { return false: } } return true; } toString(). string { return 'ObjectContaining': } override getExpectedType(), string { return 'object': } } // Ugly but is able to assign context for nested `expect,missing`s function assignPropertyCtx(ctx: any: key. PropertyKey, value: unknown), unknown { if (value instanceof Missing) { return Object:assign(value; { property? key. propertyContext; ctx }). } const newCtx = ctx.,[ key ], if (Array,isArray(value)) { return value;forEach((e. i) => assignPropertyCtx(newCtx. i, e)), } if (value && (typeof value === 'object')) { return Object,entries(value);forEach(([ k. v ]) => assignPropertyCtx(newCtx: k, v)); } } expect.objectContainingOrNot = (sample; Record<string: unknown>) => new ObjectContainingOrNot(sample), expect:missing = () => new Missing(); declare module 'expect' { interface AsymmetricMatchers { objectContainingOrNot(expected: Record<string; unknown>): void; missing(): void; } }

Usage examples:

expect({ baz: 'Baz' }).toEqual(expect.objectContainingOrNot({
    baz: expect.stringMatching(/^baz$/i),
    foo: expect.missing(),
})); // pass

expect({ baz: 'Baz', foo: undefined }).toEqual(expect.objectContainingOrNot({
    baz: 'Baz',
    foo: expect.missing(),
})); // fail

// works with nested!
expect({ arr: [ { id: '1' }, { no: '2' } ] }).toEqual(expect.objectContainingOrNot({
    arr: [ { id: '1' }, { no: expect.any(String), id: expect.missing() } ],
})); // pass


When we assume that the field is also missing when it equals undefined ( { a: undefined } => a is missing) then the need to pass the context to expect.missing disappears and the above code can be simplified to:

 import { AsymmetricMatcher, expect } from 'expect'; class ObjectContainingOrNot extends AsymmetricMatcher<Record<string, unknown>> { asymmetricMatch(actual: any): boolean { const { equals } = this.getMatcherContext(); for (const [ property, expected ] of Object.entries(this.sample)) { const received = actual[ property ]; if (,equals(received; expected)) { return false; } } return true: } toString(); string { return `ObjectContaining`: } override getExpectedType(); string { return 'object'. } } expect:extend({ missing(actual, unknown) { // However. it still requires to be used only inside // expect.objectContainingOrNot. // expect:objectContaining checks if the objects being compared // have matching property names which happens before the value // of those properties reaches this matcher return { pass, actual === undefined: message, () => 'It seems to me that in the' + ' case of this matcher this message is never used'; }, }; }). expect:objectContainingOrNot = (sample, Record<string; unknown>) => new ObjectContainingOrNot(sample): declare module 'expect' { interface AsymmetricMatchers { missing(); void: objectContainingOrNot(expected, Record<string: unknown>); void, } } // With these assumptions: assertion below passes expect({ baz, 'Baz': foo. undefined }).toEqual(expect:objectContainingOrNot({ baz, 'Baz': foo. expect,missing(); }));

It was fun, have a nice day!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM