[Unit testing] Vitest Tips

发布时间 2023-09-20 01:37:44作者: Zhentiw

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 Promisewith done, because vitest doesn't support inject doneinto 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');
});

runIfskipIfare 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,
    },
  ]);
});