Build Comprehensive Vitest Test Suites
Skill for Vitest test suites - configuration, mocking, async/timer testing, component tests, and custom matchers.
Maintainer of this project? Claim this page to edit the listing.
1.0.0Add to Favorites
Why it matters
Automate the creation of robust and maintainable test suites for modern JavaScript/TypeScript applications using Vitest. This asset ensures high code coverage, excellent performance, and a superior developer experience by generating unit and integration tests with advanced mocking strategies.
Outcomes
What it gets done
Generate unit and integration tests following Arrange-Act-Assert pattern.
Implement advanced mocking strategies for external dependencies.
Configure Vitest for optimal performance and coverage.
Write tests for asynchronous operations and component interactions.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-vitest-test-builder | bash Overview
Vitest Test Builder
A skill for Vitest test suites - configuration and coverage thresholds, mocking and spying strategies, async and fake-timer testing, React component testing, and custom matchers. Use it when writing or reviewing Vitest-specific tests, not as a general JavaScript testing theory guide or for other runners like Jest.
What it does
This skill covers Vitest, the fast test runner for modern JavaScript/TypeScript, spanning unit tests, integration tests, mocking strategies, and advanced Vitest features for comprehensive, maintainable test suites. Core principles: readable, reliable, and fast tests, the Arrange-Act-Assert pattern, descriptive test names, proper test isolation with setup/teardown, Vitest's built-in mocking and assertion utilities, and minimal coupling to implementation details.
Essential configuration is shown via a vitest.config.ts:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
export default defineConfig({
test: {
environment: 'jsdom', // or 'node', 'happy-dom'
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/'],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
alias: {
'@': resolve(__dirname, './src')
}
}
})
Test structure is demonstrated with a full service-layer suite: mocking a database-client dependency, beforeEach/afterEach setup and mock clearing, and Arrange-Act-Assert tests covering both a successful creation path and a validation-error rejection path with an assertion that the mocked insert was never called.
Advanced mocking covers partial mocking (vi.importActual to keep real exports while overriding one function), chained mock implementations (mockResolvedValueOnce twice then a final mockRejectedValue), and spying on real implementations via vi.spyOn. Async-operation testing covers awaiting a pending-then-resolved promise while asserting a loading flag, and fake-timer-based debounce testing (vi.useFakeTimers, vi.advanceTimersByTime, vi.useRealTimers). Component testing is shown via @testing-library/react and user-event, rendering a component, filling and submitting a form, and asserting the update callback received the changed data. Custom matchers and utilities cover a test-data factory with override support and an expect.extend-based custom email-validation matcher.
Performance and optimization tips: vi.hoisted() for module-level mocks needing early initialization, concurrent tests for independent suites, test.each() for parameterized testing, vi.stubGlobal() for global mocking, proper afterEach cleanup to prevent test pollution, and test.skip()/test.only() for debugging. Best practices cover test naming ("should [expected behavior] when [condition]"), specific rather than generic assertions, mocking at the boundary (external APIs, databases, file systems), prioritizing critical-path coverage over raw percentage, regularly refactoring tests alongside production code, and descriptive names and comments for complex scenarios.
When to use - and when NOT to
Use it when writing or reviewing a Vitest test suite - configuring coverage thresholds, mocking dependencies, testing async operations or timers, testing React components, or building custom matchers. A good signal it applies: a negative-path test needs to assert a side effect never happened, such as confirming a mocked database insert was never called after a validation error. It is not a general JavaScript-testing-theory guide beyond Vitest's specific API surface, and it does not cover other runners like Jest or Mocha beyond patterns that happen to overlap.
Inputs and outputs
Given a module, component, or async function to test, it produces a Vitest test file (describe/it blocks, mocks, assertions) plus the relevant vitest.config.ts settings - environment, coverage thresholds, aliases - needed to run it.
Integrations
Built on Vitest's core API (describe, it, expect, vi), with component-testing examples using @testing-library/react and @testing-library/user-event, and coverage via the v8 provider.
Who it's for
Frontend and full-stack JavaScript/TypeScript engineers who need their test suite to stay fast and isolated as it grows, using patterns like hoisted module mocks and concurrent suites rather than letting a monolithic test file slow down over time.
Source README
Vitest Test Builder Expert
You are an expert in Vitest, the blazing-fast test runner built for modern JavaScript/TypeScript applications. You specialize in creating comprehensive, maintainable test suites with excellent coverage, performance, and developer experience. Your expertise covers unit tests, integration tests, mocking strategies, and advanced Vitest features.
Core Testing Principles
- Write tests that are readable, reliable, and fast
- Follow the Arrange-Act-Assert pattern for clear test structure
- Use descriptive test names that explain the expected behavior
- Implement proper test isolation with appropriate setup/teardown
- Leverage Vitest's built-in utilities for mocking and assertions
- Design tests for maintainability with minimal coupling to implementation details
Essential Vitest Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
export default defineConfig({
test: {
environment: 'jsdom', // or 'node', 'happy-dom'
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/'],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
alias: {
'@': resolve(__dirname, './src')
}
}
})
Test Structure and Organization
// Example comprehensive test suite
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { UserService } from '@/services/UserService'
import { DatabaseClient } from '@/lib/database'
// Mock external dependencies
vi.mock('@/lib/database')
describe('UserService', () => {
let userService: UserService
let mockDb: vi.MockedObject<DatabaseClient>
beforeEach(() => {
mockDb = vi.mocked(DatabaseClient.prototype)
userService = new UserService(mockDb)
})
afterEach(() => {
vi.clearAllMocks()
})
describe('createUser', () => {
it('should create user with valid data and return user object', async () => {
// Arrange
const userData = { name: 'John Doe', email: 'john@example.com' }
const expectedUser = { id: 1, ...userData, createdAt: new Date() }
mockDb.insert.mockResolvedValue(expectedUser)
// Act
const result = await userService.createUser(userData)
// Assert
expect(result).toEqual(expectedUser)
expect(mockDb.insert).toHaveBeenCalledWith('users', userData)
})
it('should throw validation error for invalid email format', async () => {
// Arrange
const invalidUserData = { name: 'John', email: 'invalid-email' }
// Act & Assert
await expect(userService.createUser(invalidUserData))
.rejects.toThrow('Invalid email format')
expect(mockDb.insert).not.toHaveBeenCalled()
})
})
})
Advanced Mocking Strategies
// Partial mocking with vi.mocked
import { vi } from 'vitest'
import * as utils from '@/utils'
vi.mock('@/utils', async () => {
const actual = await vi.importActual('@/utils')
return {
...actual,
fetchData: vi.fn()
}
})
// Mock implementation with different behaviors
const mockFetch = vi.fn()
mockFetch
.mockResolvedValueOnce({ data: 'first call' })
.mockResolvedValueOnce({ data: 'second call' })
.mockRejectedValue(new Error('Network error'))
// Spy on real implementations
const spy = vi.spyOn(utils, 'processData')
spy.mockImplementation((data) => ({ processed: data }))
Testing Async Operations
// Testing promises and async/await
it('should handle async operations correctly', async () => {
const promise = service.asyncOperation()
// Test pending state if needed
expect(service.isLoading).toBe(true)
const result = await promise
expect(result).toEqual(expectedResult)
expect(service.isLoading).toBe(false)
})
// Testing with fake timers
it('should debounce function calls', async () => {
vi.useFakeTimers()
const callback = vi.fn()
const debouncedFn = debounce(callback, 1000)
debouncedFn('test1')
debouncedFn('test2')
debouncedFn('test3')
expect(callback).not.toHaveBeenCalled()
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('test3')
vi.useRealTimers()
})
Component Testing Patterns
// Testing React components with @testing-library
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { UserProfile } from '@/components/UserProfile'
it('should update user profile when form is submitted', async () => {
const user = userEvent.setup()
const mockOnUpdate = vi.fn()
const initialUser = { name: 'John', email: 'john@example.com' }
render(<UserProfile user={initialUser} onUpdate={mockOnUpdate} />)
const nameInput = screen.getByLabelText(/name/i)
const submitButton = screen.getByRole('button', { name: /save/i })
await user.clear(nameInput)
await user.type(nameInput, 'Jane Doe')
await user.click(submitButton)
expect(mockOnUpdate).toHaveBeenCalledWith({
...initialUser,
name: 'Jane Doe'
})
})
Custom Matchers and Utilities
// Custom test utilities
export const createTestUser = (overrides = {}) => ({
id: 1,
name: 'Test User',
email: 'test@example.com',
createdAt: new Date('2023-01-01'),
...overrides
})
// Custom matchers
expect.extend({
toBeValidEmail(received: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const pass = emailRegex.test(received)
return {
pass,
message: () =>
pass
? `Expected ${received} not to be a valid email`
: `Expected ${received} to be a valid email`
}
}
})
// Usage
expect('test@example.com').toBeValidEmail()
Performance and Optimization
- Use
vi.hoisted()for module-level mocks that need early initialization - Implement
concurrenttests for independent test suites - Leverage
test.each()for parameterized testing - Use
vi.stubGlobal()for global variable mocking - Implement proper cleanup in
afterEachto prevent test pollution - Consider
test.skip()andtest.only()for debugging
Best Practices
- Test Naming: Use "should [expected behavior] when [condition]" format
- Assertions: Make specific assertions rather than generic truthy checks
- Mocking: Mock at the boundary - external APIs, databases, file systems
- Coverage: Aim for high coverage but focus on critical paths
- Maintenance: Regularly review and refactor tests alongside production code
- Documentation: Use descriptive test names and comments for complex scenarios
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.