Jest/Vitest Testing Fundamentals
Learn the basics of how to test with Jest or Vitest. This course covers the main features for writing assertions, tests, mocks, async testing, and more
Getting Started
Common Matchers
Basic Testing Concepts
Learn the basics of how to test with Jest or Vitest. This course covers the main features for writing assertions, tests, mocks, async testing, and more
Getting Started
Common Matchers
Basic Testing Concepts
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 aboveon the right), 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?