vitest-unit-testing
Unit testing with Vitest
This skill provides patterns and conventions for writing comprehensive unit tests in TypeScript projects using Vitest.
When to use this skill
- Writing unit tests for TypeScript utilities, services, or API clients
- Testing stores (Pinia, Zustand, etc.)
- Testing API transforms and services
- Adding test coverage to existing code
- Debugging failing tests
Test file structure
Location and naming
- Place tests in
__tests__/directory next to the file being tested - Name test files with
.test.tssuffix matching the source file name - Example:
config.ts→__tests__/config.test.ts
Basic structure
import { describe, test, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { functionToTest } from '../module';
describe('Module name or function group', () => {
describe(functionName, () => {
test('should do something specific', () => {
// Arrange
const input = 'test';
// Act
const result = functionToTest(input);
// Assert
expect(result).toBe('expected');
});
});
});
Core testing patterns
AAA pattern (Arrange, Act, Assert)
Always structure tests with clear AAA sections:
test('should calculate total price correctly', () => {
// Arrange
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(250);
});
Parametrized tests with test.each
Use test.each for testing multiple scenarios:
describe(getBaseUrl, () => {
test.each([
['production', 'prod', 'https://app.example.com'],
['staging', 'staging', 'https://staging.example.com'],
['development', 'dev', 'http://localhost:3000']
])('returns correct URL for environment %s', (_name, env, expected) => {
vi.spyOn(envUtils, 'getEnvironment').mockReturnValue(env);
const url = getBaseUrl();
expect(url).toBe(expected);
});
});
Lifecycle hooks
describe('Test suite', () => {
beforeAll(() => {
// Runs once before all tests
});
beforeEach(() => {
// Runs before each test
});
afterEach(() => {
// Runs after each test — restore mocks here
vi.restoreAllMocks();
});
afterAll(() => {
// Runs once after all tests
vi.useRealTimers();
});
});
Mocking
See references/mocking.md for the comprehensive mocking guide.
Quick reference
// Mock entire module
vi.mock(import('./utils/logging'), () => ({
logException: vi.fn()
}));
// Spy on function
vi.spyOn(module, 'functionName').mockReturnValue('result');
// Spy on getter
vi.spyOn(window.location, 'hostname', 'get').mockReturnValue('test.com');
// Fake timers
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01'));
vi.advanceTimersByTimeAsync(1000); // Use async version for promise-based timers
// Cleanup
vi.restoreAllMocks();
vi.useRealTimers();
Best practices
Test naming
Use descriptive test names explaining expected behavior. Start with "should" or describe the outcome. Be specific about the scenario being tested.
// Good
test('should return empty array when no items match filter', () => {});
test('throws error when user ID is invalid', () => {});
// Bad
test('works correctly', () => {});
test('test filter', () => {});
Grouping tests
Use nested describe blocks for organization:
describe('UserService', () => {
describe(getUser, () => {
test('should fetch user by ID', () => {});
test('should handle missing user', () => {});
});
describe(updateUser, () => {
test('should update user data', () => {});
test('should validate input before update', () => {});
});
});
Test coverage
Always test:
- Expected/happy path behavior
- Error conditions
- Edge cases (null, undefined, empty values)
- Boundary conditions
Mock management
- Always restore mocks after tests using
afterEachorafterAll - Use
vi.hoisted()for shared mocks referenced in module mocks - Be specific with mock return values relevant to the test
- Only mock external dependencies, not the code under test
Assertions
Prefer specific matchers:
// Good — specific matchers
expect(result).toBe(true);
expect(array).toHaveLength(3);
expect(object).toStrictEqual({ id: 1, name: 'test' });
expect(fn).toHaveBeenCalledWith('expected-arg');
expect(fn).toHaveBeenCalledTimes(1);
// Less specific but sometimes necessary
expect(result).toBeTruthy();
expect(result).toBeFalsy();
Running tests
Always use the runTests tool instead of running tests manually in terminal. The tool automatically runs tests in non-interactive mode and provides structured output for validation.
If you must use terminal commands, use the CI/non-interactive mode of the project's test script (e.g., vitest run or the project's equivalent). Never use a command that starts interactive watch mode.
# ❌ NEVER use watch mode
npx vitest
# ✅ Use run mode for specific test files
npx vitest run src/utils/__tests__/config.test.ts
# Run with coverage
npx vitest run --coverage
# Run all tests in CI mode
npx vitest run
Check the project's package.json for available test scripts — many projects define shortcuts like test:unit, test:unit:ci, or similar.
Common pitfalls to avoid
- Don't forget to restore mocks — Always use
afterEachorafterAllwithvi.restoreAllMocks() - Don't test implementation details — Test behavior, not internal workings
- Don't create interdependent tests — Each test should be independent
- Don't mock everything — Only mock external dependencies, not the code under test
- Don't write tests that pass without assertions — Every test needs at least one
expect() - Don't use real timers when testing time-dependent code — Use
vi.useFakeTimers()
TypeScript considerations
- Use
// @ts-expect-errorwhen intentionally passing invalid types to test error handling - Use
vi.mocked()for type-safe access to mocked functions - Use
satisfiesfor type-checked assertion payloads without losing literal types
Reference files
- references/mocking.md — Comprehensive mocking guide covering module mocks, spying, time mocking, async mocks, cleanup, and mock assertions
More from perdolique/workflow
pr-creator
Create GitHub pull requests from code changes via API or generate PR content in chat. Use when user wants to create/open/submit PR, mentions pull request/PR/merge request/code review, or asks to show/generate/display/output PR content in chat (give me PR, PR to chat, send PR to chat, etc).
56commit-creator
Create English conventional commit messages for the current changes. Use when the user wants to commit code, asks for a commit message, or needs monorepo scopes and version updates handled correctly.
47code-style-typescript
TypeScript style rules for writing, reviewing, and refactoring `.ts` code. Use when working on TypeScript formatting, semicolon conventions, object layout, function call structure, or interface definitions. Also use when reviewing or writing TypeScript interfaces, type aliases, or any nested object type shapes.
38markdownlint
Configure, manage, and troubleshoot markdownlint in projects. Use when user wants to setup/install/configure markdownlint, add/remove/modify linting rules, fix markdown validation issues, customize .markdownlint.yaml, update ignore patterns, integrate with tools (Husky, CI), or troubleshoot markdown linting errors. Use even when user mentions markdown formatting problems, quality issues, or style consistency without explicitly saying "markdownlint".
30playwright-e2e-testing
Write and maintain Playwright end-to-end tests for web apps. Use when the user asks for browser or E2E coverage, or for tests covering pages, routes, redirects, navigation, dialogs, authentication, or multi-step user flows, even if they do not explicitly mention Playwright. Also use for API mocking, fixtures, and Playwright-specific assertions.
15drizzle-orm
Drizzle ORM query patterns for TypeScript. Use when writing, reviewing, or debugging Drizzle queries, especially when choosing between relational queries and the SQL builder, building dynamic filters, loading relations, or fixing Drizzle query-shape and type mismatches. Also apply during code review when a file contains non-trivial query construction with `drizzle-orm`.
11