Testing Objects

Objects are everywhere in your applications, and testing them in a simple and clear way can make your tests much easier to read and maintain.

In this lesson you will see how to do different comparisons on objects in both Jest and Vitest.

Check an object is the same as another object

If you watched the previous lesson on arrays, this will look familiar.

To check if an object is the exact same object as another, you can use the .toBe() matcher.

const exampleInput = {
  name: 'Marge',
  age: 99,
};

const returnSameObject = input => input;

test('it returns exact same object', () => {
  expect(
    returnSameObject(exampleInput)
  ).toBe(exampleInput);
});

But it is more common to check the contents are the same.

Check object's contents are the same

You can use toEqual and toStrictEqual to check that the contents of an object match

const exampleInput = {
  name: 'Marge',
  age: 99,
};

const returnNewObject = input => {
  return { ...input };
};

test('it returns object with same contents', () => {
  expect(
    returnNewObject(exampleInput)
  ).toStrictEqual(exampleInput);
  expect(
    returnNewObject(exampleInput)
  ).toEqual(exampleInput);

  // this would fail as it is a different object
  // (toBe uses Object.is for identity)
  // expect(returnNewObject(exampleInput))
  //   .toBe(exampleInput) // ❌ fail
});

Difference between toStrictEqual and toEqual on objects in Jest or Vitest

This is very similar to the difference on arrays (which are after all just objects!)

class User {
  constructor(name) {
    this.name = name;
  }
}

test('class instances vs plain objects', () => {
  const userInstance = new User(
    'Marge'
  );
  const plainObject = { name: 'Marge' };

  // toEqual doesn't care that one is a class (prototype
  // is User), and one is a plain JS object
  expect(userInstance).toEqual(
    plainObject
  );

  // this will fail - as the prototype is different:
  // expect(userInstance).toStrictEqual(plainObject);
});

Note: toStrictEqual also cares about undefined values (whereas toEqual doesn't).

test('nested objects with undefined properties', () => {
  const obj1 = {
    user: { name: 'Marge' },
    settings: { theme: 'dark' },
  };

  const obj2 = {
    user: { name: 'Marge' },
    settings: {
      theme: 'dark',
      notifications: undefined,
    },
  };

  expect(obj1).toEqual(obj2);
  // this will not work:
  // expect(obj1).toStrictEqual(obj2);
});

Testing partial object matching with objectContaining

Sometimes you only care about specific properties in an object, not the entire object.

You can use expect.objectContaining() for this.

test('checking a subset of the object matches', () => {
  const user = {
    id: 123,
    name: 'Marge',
    age: 30,
    email: 'Marge@example.com',
    lastLogin: '2024-01-15',
  };

  // We only care that it has name and age properties
  expect(user).toEqual(
    expect.objectContaining({
      name: 'Marge',
      age: 30,
    })
  );
});

This is often used and is very useful when writing a test for a function that returns lots of properties, but for the sake of the test you only care about one or two. Instead of making your test long, hard to read, and require updates if any of those other properties change, you can just assert that some of the object matches.

const createUser = (name, age) => {
  return {
    id: Math.random(), // don't want to test this
    name,
    age,
    createdAt: new Date(), // or this
    isActive: true,
  };
};

test('createUser returns object with name and age', () => {
  const result = createUser('Bob', 25);

  expect(result).toEqual(
    expect.objectContaining({
      name: 'Bob',
      age: 25,
      isActive: true,
      // not testing id/createdAt
    })
  );
});

Testing the shape

You can use assertions like expect.any(String), when you don't care about the specific value:

expect(user).toEqual(
  expect.objectContaining({
    name: expect.any(String),
    age: expect.any(Number),
    id: expect.any(Number),
  })
);

Testing an object has a property

You can use toHaveProperty. In my experience this isn't commonly used.

const user = {
  name: 'Marge',
  age: 30,
  email: 'Marge@example.com',
};

test('user has specific properties', () => {
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty(
    'age',
    30
  );
  expect(user).toHaveProperty('email');

  // this is more common:
  expect(user.age).toBe(30);
});

Dot syntax property assertions

You can also use toHaveProperty() with a dot syntax for nested properties. Again you lose any type checks.

test('deeply nested property paths', () => {
  const data = {
    user: {
      profile: {
        settings: {
          theme: 'dark',
        },
      },
    },
  };

  expect(data).toHaveProperty(
    // Note: string paths won't be type-checked and can become brittle on refactors.
    'user.profile.settings.theme',
    'dark'
  );

  // the following 2 assertions are also testing the same thing:
  expect(
    data.user.profile.settings
  ).toHaveProperty('theme', 'dark');
  expect(
    data.user.profile.settings.theme
  ).toBe('dark');
});

Comparing toHaveProperty vs just accessing it directly on the property

In TypeScript, expect(user.age)... preserves compile-time checks, whereas expect(user).toHaveProperty('age', ...) does not participate in type checking.

toHaveProperty is often more useful when combined with .not.toHaveProperty() (we will cover negative assertions later on).

Lastly, the other advantage of using toHaveProperty is nicer error messages.

test('missing property error messages', () => {
  const user = { name: 'Marge' };

  // toHaveProperty gives: "Expected object to have property 'age'"
  expect(user).toHaveProperty('age');

  // direct access gives: "Expected undefined to be 30"
  expect(user.age).toBe(30);
});

Testing with toMatchObject

toMatchObject is similar to objectContaining but it has a slightly different syntax.

It checks that the passed in object to toMatchObject(...) contains all the properties of the expected object.

test('toMatchObject for partial matching', () => {
  const user = {
    id: 123,
    name: 'Marge',
    age: 30,
    lastLogin: '2024-01-15',
  };

  expect(user).toMatchObject({
    name: 'Marge',
    age: 30,
  });
});

It is important to understand the difference between toMatchObject and objectContaining:

test('comparing toMatchObject vs objectContaining', () => {
  const user = {
    id: 123,
    name: 'Marge',
    age: 30,
  };

  expect(user).toMatchObject({
    name: 'Marge',
    age: 30,
  });

  expect(user).toEqual(
    expect.objectContaining({
      name: 'Marge',
      age: 30,
    })
  );
});

Lesson Task

In the code editor (

use the tabs above

), update the tests so they pass. For the final test, make sure to use expect.objectContaining(...) inside the toEqual(...).

Ready to try running tests for this lesson?