[Unit testing] Vitest Tips
1. Globally import
In vitest, you need to do
import { it, expect, test } from 'vitest';
In every test files, If you don't want to do it you can set configuration:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});
But it might have some problem with 3rd-party addons... so normally we need to import in each file.
2. Expect a test to fail
A test passes if it doesn't fail.
import { it, expect, test } from 'vitest';
it.fails('should be able to expect a test to fail', () => {
expect(false).toBe(true);
});
Sometime you might need it to make sure you are testing what you want to avoid test passing by accident.
3. Test async code
test('works when returning a promise', () => {
return new Promise((done) => {
setTimeout(() => {
expect('This should fail.').not.toBe('Totally not the same.');
done(null);
}, 0);
});
});
You need to add a extra Promise
with done
, because vitest doesn't support inject done
into test suit calback.
4. Run test by condition
// npx vitest --mode=development --run --reporter=verbose
test.runIf(process.env.NODE_ENV === 'development')(
'it should run in development',
() => {
expect(process.env.NODE_ENV).toBe('development');
},
);
// npx vitest --run --reporter=verbose
test.skipIf(process.env.NODE_ENV !== 'test')('it should run in test', () => {
expect(process.env.NODE_ENV).toBe('test');
});
runIf
, skipIf
are helper methods.
5. UI & reporters
npx vitest --run --reporter=verbose // --run: single run without watcher mode
npx vitest --run --reporter-dot. // --dot: ......... each dot represent one test
npx vitest --ui // run in watch mode and open ui
6. Test passes when it doesn't fail
Insist on having expectations, this is typically something I only use while debuggin:
expect.hasAssertions()
expect.assertions(1)
https://vitest.dev/api/expect.html#expect-assertions
https://vitest.dev/api/expect.html#expect-hasassertions
7. Expect
import { describe, expect, it } from 'vitest';
import { createPerson, Person } from '$lib/person';
import { KanbanBoard } from '$lib/kanban-board';
/**
* toBe: https://vitest.dev/api/expect.html#tobe
* toBeCloseTo: https://vitest.dev/api/expect.html#tobecloseto
* toBeInstanceOf: https://vitest.dev/api/expect.html#tobeinstanceof
* toBeUndefined: https://vitest.dev/api/expect.html#tobeundefined
* toContain: https://vitest.dev/api/expect.html#tocontain
* toThrow: https://vitest.dev/api/expect.html#tothrow
* toThrowError: https://vitest.dev/api/expect.html#tothrowerror
*/
test('should pass if the two numbers would add up correctly in a language other than JavaScript', () => {
expect(0.2 + 0.1).toBeCloseTo(0.3);
});
describe('createPerson', () => {
test('should create an instance of a person', () => {
const person = createPerson('Ada Lovelace');
expect.hasAssertions();
// Verify that person is an instance of a Person.
expect(person).toBeInstanceOf(Person);
});
});
describe('Kanban Board', () => {
test('should include "Backlog" in board.statuses', () => {
const board = new KanbanBoard('Things to Do');
expect.hasAssertions();
// Verify that board.statuses contains "Backlog".
expect(board.statuses).toContain('Backlog');
});
test('should *not* include "Bogus" in board.statuses', () => {
const board = new KanbanBoard('Things to Do');
expect.hasAssertions();
// Verify that board.statuses does not contain "Bogus".
expect(board.statuses).not.toContain('Bogus');
});
test('should include an added status in board.statuses using #addStatus', () => {
const board = new KanbanBoard('Things to Do');
expect.hasAssertions();
// Use board.addStatus to add a status.
// Verify that the new status is—in fact—now in board.statuses.
board.addStatus('greatOne');
expect(board.statuses).toContain('greatOne');
});
test('should remove a status using #removeStatus', () => {
const board = new KanbanBoard('Things to Do');
expect.hasAssertions();
// Use board.removeStatus to remove a status.
board.removeStatus('Backlog');
// Verify that the status is no longer in in board.statuses.
expect(board.statuses).not.toContain('Backlog');
});
});
describe('Person', () => {
test('will create a person with a first name', () => {
const person = new Person('Madonna');
expect.hasAssertions();
// Verify that person.firstName is correct.
expect(person.firstName).toBe('Madonna');
});
test('will create a person with a first and last name', () => {
const person = new Person('Madonna Cicone');
expect.hasAssertions();
// Verify that person.lastName is correct.
expect(person.lastName).toBe('Cicone');
});
test('will create a person with a first, middle, and last name', () => {
const person = new Person('Madonna Louise Cicone');
expect.hasAssertions();
// Verify that person.middleName is correct.
expect(person.middleName).toBe('Louise');
});
test('will throw if you provide an empty string', () => {
const fn = () => {
new Person('');
};
expect.hasAssertions();
// Verify that function above throws.
expect(fn).toThrow();
});
test('will throw a specific error message if you provide an empty string', () => {
const errorMessage = 'fullName cannot be an empty string';
const fn = () => {
new Person('');
};
expect.hasAssertions();
// Verify that function above throws the error message above.
expect(fn).toThrowError(errorMessage);
});
test('will add a friend', () => {
const john = new Person('John Lennon');
const paul = new Person('Paul McCartney');
john.addFriend(paul);
expect.hasAssertions();
// Verify that john.friends contains paul.
expect(john.friends).toContain(paul);
});
test('will mutually add a friend', () => {
const john = new Person('John Lennon');
const paul = new Person('Paul McCartney');
john.addFriend(paul);
expect.hasAssertions();
// Verify that paul.friends contains john.
expect(paul.friends).toContain(john);
});
it.todo('will remove a friend', () => {
const john = new Person('John Lennon');
const paul = new Person('Paul McCartney');
john.addFriend(paul);
john.removeFriend(paul);
expect.hasAssertions();
// Verify that john.friends does not inclide paul.
expect(john.friends).not.toContain(paul);
});
test('will mutually remove friends', () => {
const john = new Person('John Lennon');
const paul = new Person('Paul McCartney');
john.addFriend(paul);
john.removeFriend(paul);
expect.hasAssertions();
// Verify that paul.friends does not include john.
expect(paul.friends).not.toContain(john);
});
});
const explode = () => {
throw new Error('Something went terribly wrong');
};
describe('explode', () => {
test('should throw an error', () => {
expect(() => explode()).toThrowError('Something went terribly wrong');
});
test('should throw a specific error containing "terribly wrong"', () => {
expect(() => explode()).toThrowError(
expect.objectContaining({
message: expect.stringContaining('terribly wrong'),
}),
);
});
});
8. Asymmetric matchers
https://vitest.dev/api/expect.html#expect-arraycontaining
https://vitest.dev/api/expect.html#expect-any
import { v4 as id } from 'uuid';
import { expect, it } from 'vitest';
type ComputerScientist = {
id: string;
firstName: string;
lastName: string;
isCool?: boolean;
};
const createComputerScientist = (
firstName: string,
lastName: string,
): ComputerScientist => ({ id: 'cs-' + id(), firstName, lastName });
const addToCoolKidsClub = (p: ComputerScientist, club: unknown[]) => {
club.push({ ...p, isCool: true });
};
it('include cool computer scientists by virtue of them being in the club', () => {
const people: ComputerScientist[] = [];
addToCoolKidsClub(createComputerScientist('Grace', 'Hopper'), people);
addToCoolKidsClub(createComputerScientist('Ada', 'Lovelace'), people);
addToCoolKidsClub(createComputerScientist('Annie', 'Easley'), people);
addToCoolKidsClub(createComputerScientist('Dorothy', 'Vaughn'), people);
for (const person of people) {
expect(typeof person.firstName).toBe('string');
expect(typeof person.lastName).toBe('string');
expect(person.isCool).toBe(true);
}
for (const person of people) {
expect(person).toEqual({
id: expect.stringMatching(/^cs-/),
firstName: expect.any(String),
lastName: expect.any(String),
isCool: true,
});
}
for (const person of people) {
expect(person).toEqual(
expect.objectContaining({
isCool: expect.any(Boolean),
}),
);
}
});
import reducer, {
add,
remove,
toggle,
markAllAsUnpacked,
update,
} from './items-slice';
it('returns an empty array as the initial state', () => {
expect(reducer(undefined, { type: 'noop' })).toEqual([]);
});
it('supports adding an item with the correct name', () => {
expect(reducer([], add({ name: 'iPhone' }))).toEqual([
expect.objectContaining({ name: 'iPhone' }),
]);
});
it('prefixes ids with "item-"', () => {
expect(reducer([], add({ name: 'iPhone' }))).toEqual([
expect.objectContaining({ id: expect.stringMatching(/^item-/) }),
]);
});
it('defaults new items to a packed status of false', () => {
expect(reducer([], add({ name: 'iPhone' }))).toEqual([
expect.objectContaining({ packed: false }),
]);
});
it('supports removing an item', () => {
const state = [
{
id: '1',
name: 'iPhone',
packed: false,
},
];
const result = reducer(state, remove({ id: '1' }));
expect(result).toEqual([]);
});
it('supports toggling an item', () => {
const state = [
{
id: '1',
name: 'iPhone',
packed: false,
},
];
const result = reducer(state, toggle({ id: '1' }));
expect(result).toEqual([
{
id: '1',
name: 'iPhone',
packed: true,
},
]);
});
it('supports updating an item', () => {
const state = [
{
id: '1',
name: 'iPhone',
packed: false,
},
];
const result = reducer(
state,
update({ id: '1', name: 'Samsung Galaxy S23' }),
);
expect(result).toEqual([
{
id: '1',
name: 'Samsung Galaxy S23',
packed: false,
},
]);
});
it('supports marking all items as unpacked', () => {
const state = [
{
id: '1',
name: 'iPhone',
packed: true,
},
{
id: '2',
name: 'iPhone Charger',
packed: true,
},
];
const result = reducer(state, markAllAsUnpacked());
expect(result).toEqual([
{
id: '1',
name: 'iPhone',
packed: false,
},
{
id: '2',
name: 'iPhone Charger',
packed: false,
},
]);
});