🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Vitest
Exemples de framework de testing JavaScript moderne avec intégration Vite, featuring unit tests, component testing, et patterns de testing modernes
💻 Configuration de Base Vitest typescript
🟢 simple
⭐⭐
Configuration complète de Vitest et configuration de testing de base avec support TypeScript
⏱️ 15 min
🏷️ vitest, testing, setup
Prerequisites:
Node.js, npm/pnpm, Basic testing concepts
// Vitest Basic Setup Example
// 1. Installation
// npm install -D vitest jsdom @testing-library/vue @testing-library/react
// or: pnpm add -D vitest jsdom @testing-library/vue @testing-library/react
// 2. vitest.config.ts - Configuration file
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
vue(), // For Vue projects
react(), // For React projects
],
test: {
// Global test environment
environment: 'jsdom', // or 'node' for backend testing
// Setup files to run before tests
setupFiles: ['./src/test/setup.ts'],
// Coverage configuration
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*'
]
},
// Test matching patterns
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: ['node_modules/', 'dist/', '.idea/', '.git/', '.cache/'],
// Watch mode settings
watch: false, // Set true for development
// Global variables available in tests
globals: true,
// Timeout for each test (ms)
testTimeout: 10000,
// Hook timeout (ms)
hookTimeout: 10000
},
// Regular Vite configuration
resolve: {
alias: {
'@': '/src'
}
}
})
// 3. src/test/setup.ts - Test setup file
import { vi } from 'vitest'
// Mock global APIs
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
}
vi.stubGlobal('localStorage', localStorageMock)
// Mock fetch
global.fetch = vi.fn()
// 4. Package.json scripts
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch"
}
}
// 5. tsconfig.json - Add to compilerOptions
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
💻 Testing Unitaire Vitest typescript
🟡 intermediate
⭐⭐⭐
Exemples complets de testing unitaire avec assertions, mocks, et testing asynchrone
⏱️ 25 min
🏷️ vitest, unit tests, mocks
Prerequisites:
Basic Vitest setup, JavaScript/TypeScript, Testing concepts
// Vitest Unit Testing Examples
// 1. src/calculator.ts - Code to test
export class Calculator {
add(a: number, b: number): number {
return a + b
}
subtract(a: number, b: number): number {
return a - b
}
multiply(a: number, b: number): number {
return a * b
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero is not allowed')
}
return a / b
}
async asyncAdd(a: number, b: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(a + b), 100)
})
}
factorial(n: number): number {
if (n < 0) throw new Error('Factorial of negative number')
if (n === 0) return 1
return n * this.factorial(n - 1)
}
}
// 2. src/calculator.test.ts - Test file
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { Calculator } from './calculator'
describe('Calculator', () => {
let calculator: Calculator
beforeEach(() => {
calculator = new Calculator()
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Basic arithmetic operations', () => {
it('should add two numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5)
expect(calculator.add(-1, 1)).toBe(0)
expect(calculator.add(0, 0)).toBe(0)
})
it('should subtract two numbers correctly', () => {
expect(calculator.subtract(5, 3)).toBe(2)
expect(calculator.subtract(0, 5)).toBe(-5)
expect(calculator.subtract(-2, -3)).toBe(1)
})
it('should multiply two numbers correctly', () => {
expect(calculator.multiply(3, 4)).toBe(12)
expect(calculator.multiply(-2, 3)).toBe(-6)
expect(calculator.multiply(0, 5)).toBe(0)
})
it('should divide two numbers correctly', () => {
expect(calculator.divide(10, 2)).toBe(5)
expect(calculator.divide(-6, 3)).toBe(-2)
expect(calculator.divide(7, 2)).toBe(3.5)
})
it('should throw error when dividing by zero', () => {
expect(() => calculator.divide(5, 0)).toThrow('Division by zero is not allowed')
})
})
describe('Edge cases and special values', () => {
it('should handle decimal numbers', () => {
expect(calculator.add(2.5, 1.5)).toBeCloseTo(4.0)
expect(calculator.multiply(2.5, 2)).toBe(5.0)
})
it('should handle very large numbers', () => {
const largeNumber = Number.MAX_SAFE_INTEGER
expect(calculator.add(largeNumber, 1)).toBe(largeNumber + 1)
})
})
describe('Async operations', () => {
it('should add numbers asynchronously', async () => {
const result = await calculator.asyncAdd(2, 3)
expect(result).toBe(5)
})
it('should handle async operations with timeout', async () => {
const startTime = Date.now()
await calculator.asyncAdd(1, 2)
const endTime = Date.now()
expect(endTime - startTime).toBeGreaterThanOrEqual(100)
})
})
describe('Recursive operations', () => {
it('should calculate factorial correctly', () => {
expect(calculator.factorial(0)).toBe(1)
expect(calculator.factorial(1)).toBe(1)
expect(calculator.factorial(5)).toBe(120)
expect(calculator.factorial(10)).toBe(3628800)
})
it('should throw error for negative factorial', () => {
expect(() => calculator.factorial(-1)).toThrow('Factorial of negative number')
})
})
})
// 3. Advanced assertions examples
import { describe, it, expect } from 'vitest'
describe('Advanced Assertions', () => {
it('demonstrates various matchers', () => {
const user = {
id: 1,
name: 'John Doe',
email: '[email protected]',
preferences: {
theme: 'dark',
notifications: true
},
tags: ['developer', 'javascript']
}
// Basic equality
expect(user.name).toBe('John Doe')
// Object containing
expect(user).toEqual(
expect.objectContaining({
name: 'John Doe',
email: '[email protected]'
})
)
// Array containing
expect(user.tags).toEqual(
expect.arrayContaining(['developer'])
)
// String matching
expect(user.email).toMatch(/@example\.com$/)
expect(user.email).toContain('@')
// Number comparisons
expect(user.id).toBeGreaterThan(0)
expect(user.id).toBeLessThan(100)
// Truthiness
expect(user.preferences.notifications).toBeTruthy()
expect(user.preferences.theme).toBeDefined()
// Type checking
expect(typeof user.name).toBe('string')
expect(Array.isArray(user.tags)).toBe(true)
// Custom matchers
expect(user.tags.length).toBe(2)
})
it('demonstrates negated matchers', () => {
const value = 'hello'
expect(value).not.toBe('world')
expect(value).not.toMatch(/[0-9]/)
expect(value).not.toBeNull()
expect(value).not.toBeUndefined()
})
})
// 4. Mock functions examples
import { describe, it, expect, vi } from 'vitest'
describe('Mock Functions', () => {
it('should create and track mock function calls', () => {
const mockFn = vi.fn()
mockFn()
mockFn('hello')
mockFn('world', 123)
// Call counts
expect(mockFn).toHaveBeenCalledTimes(3)
// Call arguments
expect(mockFn).toHaveBeenNthCalledWith(1)
expect(mockFn).toHaveBeenNthCalledWith(2, 'hello')
expect(mockFn).toHaveBeenNthCalledWith(3, 'world', 123)
})
it('should use mock return values', () => {
const mockFn = vi.fn()
.mockReturnValueOnce(42)
.mockReturnValueOnce(100)
.mockReturnValue('default')
expect(mockFn()).toBe(42)
expect(mockFn()).toBe(100)
expect(mockFn()).toBe('default')
expect(mockFn()).toBe('default')
})
it('should use mock resolved values', async () => {
const asyncMock = vi.fn()
.mockResolvedValueOnce('first result')
.mockResolvedValueOnce('second result')
.mockRejectedValueOnce(new Error('Async error'))
expect(await asyncMock()).toBe('first result')
expect(await asyncMock()).toBe('second result')
await expect(asyncMock()).rejects.toThrow('Async error')
})
it('should mock implementation', () => {
const addMock = vi.fn((a: number, b: number) => a + b)
expect(addMock(2, 3)).toBe(5)
expect(addMock).toHaveBeenCalledWith(2, 3)
expect(addMock).toHaveBeenCalledTimes(1)
})
})
💻 Testing de Composants Vitest typescript
🟡 intermediate
⭐⭐⭐
Exemples de testing de composants React et Vue avec Vitest et Testing Library
⏱️ 30 min
🏷️ vitest, component testing, react, vue
Prerequisites:
React/Vue basics, Vitest setup, Testing Library
// Vitest Component Testing Examples
// 1. React Component Testing Example
// src/components/Counter.tsx
import React, { useState } from 'react'
interface CounterProps {
initialValue?: number
onCountChange?: (count: number) => void
}
export const Counter: React.FC<CounterProps> = ({
initialValue = 0,
onCountChange
}) => {
const [count, setCount] = useState(initialValue)
const increment = () => {
const newCount = count + 1
setCount(newCount)
onCountChange?.(newCount)
}
const decrement = () => {
const newCount = count - 1
setCount(newCount)
onCountChange?.(newCount)
}
const reset = () => {
const newCount = initialValue
setCount(newCount)
onCountChange?.(newCount)
}
return (
<div className="counter">
<h2>Counter: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}
// src/components/Counter.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from './Counter'
describe('Counter Component', () => {
it('renders with initial value', () => {
render(<Counter initialValue={5} />)
expect(screen.getByText('Counter: 5')).toBeInTheDocument()
expect(screen.getByText('+')).toBeInTheDocument()
expect(screen.getByText('-')).toBeInTheDocument()
expect(screen.getByText('Reset')).toBeInTheDocument()
})
it('increments count when + button is clicked', () => {
render(<Counter initialValue={0} />)
const incrementButton = screen.getByText('+')
fireEvent.click(incrementButton)
expect(screen.getByText('Counter: 1')).toBeInTheDocument()
})
it('decrements count when - button is clicked', () => {
render(<Counter initialValue={10} />)
const decrementButton = screen.getByText('-')
fireEvent.click(decrementButton)
expect(screen.getByText('Counter: 9')).toBeInTheDocument()
})
it('resets count when reset button is clicked', () => {
render(<Counter initialValue={5} />)
// First increment
fireEvent.click(screen.getByText('+'))
expect(screen.getByText('Counter: 6')).toBeInTheDocument()
// Then reset
fireEvent.click(screen.getByText('Reset'))
expect(screen.getByText('Counter: 5')).toBeInTheDocument()
})
it('calls onCountChange callback when count changes', () => {
const mockOnChange = vi.fn()
render(<Counter initialValue={0} onCountChange={mockOnChange} />)
fireEvent.click(screen.getByText('+'))
expect(mockOnChange).toHaveBeenCalledWith(1)
fireEvent.click(screen.getByText('-'))
expect(mockOnChange).toHaveBeenCalledWith(-1)
fireEvent.click(screen.getByText('Reset'))
expect(mockOnChange).toHaveBeenCalledWith(0)
})
})
// 2. Vue Component Testing Example
// src/components/UserProfile.vue
<template>
<div class="user-profile">
<div v-if="loading" class="loading">Loading...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else class="profile">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<div class="stats">
<span>Followers: {{ user.followers }}</span>
<span>Following: {{ user.following }}</span>
</div>
<button @click="toggleFollow">
{{ isFollowing ? 'Unfollow' : 'Follow' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
avatar: string
followers: number
following: number
}
interface Props {
userId: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
follow: [{ userId: number, isFollowing: boolean }]
}>()
const loading = ref(true)
const error = ref<string | null>(null)
const user = ref<User | null>(null)
const isFollowing = ref(false)
const followCount = computed(() => {
return user.value ? user.value.followers + (isFollowing.value ? 1 : 0) : 0
})
const loadUser = async () => {
try {
loading.value = true
error.value = null
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
// Mock data
user.value = {
id: props.userId,
name: `User ${props.userId}`,
email: `user${props.userId}@example.com`,
avatar: `https://i.pravatar.cc/150?img=${props.userId}`,
followers: 100,
following: 50
}
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load user'
} finally {
loading.value = false
}
}
const toggleFollow = () => {
if (user.value) {
isFollowing.value = !isFollowing.value
emit('follow', {
userId: user.value.id,
isFollowing: isFollowing.value
})
}
}
loadUser()
</script>
// src/components/UserProfile.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import UserProfile from './UserProfile.vue'
describe('UserProfile Component', () => {
it('shows loading state initially', () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
expect(wrapper.find('.loading').exists()).toBe(true)
expect(wrapper.text()).toContain('Loading...')
})
it('displays user profile after loading', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
// Wait for async data loading
await new Promise(resolve => setTimeout(resolve, 1100))
await nextTick()
expect(wrapper.find('.loading').exists()).toBe(false)
expect(wrapper.find('.profile').exists()).toBe(true)
expect(wrapper.text()).toContain('User 1')
expect(wrapper.text()).toContain('[email protected]')
})
it('shows follow button and handles clicks', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
await new Promise(resolve => setTimeout(resolve, 1100))
await nextTick()
const followButton = wrapper.find('button')
expect(followButton.exists()).toBe(true)
expect(followButton.text()).toBe('Follow')
await followButton.trigger('click')
expect(followButton.text()).toBe('Unfollow')
const emitted = wrapper.emitted('follow')
expect(emitted).toBeDefined()
expect(emitted![0]).toEqual([{ userId: 1, isFollowing: true }])
})
it('calculates follower count correctly', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
await new Promise(resolve => setTimeout(resolve, 1100))
await nextTick()
expect(wrapper.text()).toContain('Followers: 100')
await wrapper.find('button').trigger('click')
await nextTick()
expect(wrapper.text()).toContain('Followers: 101')
})
})
// 3. Custom Hooks Testing Example
// src/hooks/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue: number = 0) {
const count = ref(initialValue)
const increment = () => {
count.value += 1
}
const decrement = () => {
count.value -= 1
}
const reset = () => {
count.value = initialValue
}
const setValue = (value: number) => {
count.value = value
}
const isEven = computed(() => count.value % 2 === 0)
const isPositive = computed(() => count.value > 0)
return {
count: readonly(count),
increment,
decrement,
reset,
setValue,
isEven,
isPositive
}
}
// src/hooks/useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
import { ref } from 'vue'
describe('useCounter Hook', () => {
it('initializes with default value', () => {
const { count, isEven, isPositive } = useCounter()
expect(count.value).toBe(0)
expect(isEven.value).toBe(true)
expect(isPositive.value).toBe(false)
})
it('initializes with custom value', () => {
const { count } = useCounter(5)
expect(count.value).toBe(5)
})
it('increments count correctly', () => {
const { count, increment } = useCounter(2)
increment()
expect(count.value).toBe(3)
increment()
expect(count.value).toBe(4)
})
it('decrements count correctly', () => {
const { count, decrement } = useCounter(5)
decrement()
expect(count.value).toBe(4)
decrement()
expect(count.value).toBe(3)
})
it('resets count to initial value', () => {
const { count, increment, reset } = useCounter(10)
increment()
increment()
expect(count.value).toBe(12)
reset()
expect(count.value).toBe(10)
})
it('sets count to specific value', () => {
const { count, setValue } = useCounter()
setValue(100)
expect(count.value).toBe(100)
})
it('computes even/odd correctly', () => {
const { count, increment, isEven } = useCounter(0)
expect(isEven.value).toBe(true)
increment()
expect(isEven.value).toBe(false)
increment()
expect(isEven.value).toBe(true)
})
})
💻 Patterns de Testing Avancés Vitest typescript
🔴 complex
⭐⭐⭐⭐
Patterns de testing avancés incluant integration tests, performance tests, et utilitaires de testing
⏱️ 40 min
🏷️ vitest, integration, performance, advanced
Prerequisites:
Advanced Vitest, TypeScript, Testing patterns
// Vitest Advanced Testing Patterns
// 1. Integration Testing Example
// src/services/userService.ts
export interface User {
id: number
name: string
email: string
role: 'user' | 'admin'
}
export interface UserRepository {
findById(id: number): Promise<User | null>
create(userData: Omit<User, 'id'>): Promise<User>
update(id: number, userData: Partial<User>): Promise<User>
delete(id: number): Promise<boolean>
}
export class UserService {
constructor(private repository: UserRepository) {}
async getUser(id: number): Promise<User | null> {
const user = await this.repository.findById(id)
if (!user) {
throw new Error(`User with id ${id} not found`)
}
return user
}
async createUser(userData: Omit<User, 'id'>): Promise<User> {
if (!userData.email?.includes('@')) {
throw new Error('Invalid email format')
}
return this.repository.create(userData)
}
async updateUserRole(id: number, role: User['role']): Promise<User> {
const user = await this.getUser(id)
return this.repository.update(id, { role })
}
async deleteUser(id: number): Promise<boolean> {
await this.getUser(id) // Will throw if user doesn't exist
return this.repository.delete(id)
}
}
// src/services/userService.integration.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { UserService, User, UserRepository } from './userService'
// Mock repository for testing
class MockUserRepository implements UserRepository {
private users: User[] = []
private nextId = 1
async findById(id: number): Promise<User | null> {
return this.users.find(user => user.id === id) || null
}
async create(userData: Omit<User, 'id'>): Promise<User> {
const user: User = {
id: this.nextId++,
...userData
}
this.users.push(user)
return user
}
async update(id: number, userData: Partial<User>): Promise<User> {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with id ${id} not found`)
}
this.users[userIndex] = { ...this.users[userIndex], ...userData }
return this.users[userIndex]
}
async delete(id: number): Promise<boolean> {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) return false
this.users.splice(userIndex, 1)
return true
}
clear() {
this.users = []
this.nextId = 1
}
}
describe('UserService Integration Tests', () => {
let userService: UserService
let mockRepository: MockUserRepository
beforeEach(() => {
mockRepository = new MockUserRepository()
userService = new UserService(mockRepository)
mockRepository.clear()
})
describe('User CRUD operations', () => {
it('should create and retrieve a user', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
role: 'user' as const
}
const createdUser = await userService.createUser(userData)
expect(createdUser.id).toBe(1)
expect(createdUser.name).toBe('John Doe')
const retrievedUser = await userService.getUser(createdUser.id)
expect(retrievedUser).toEqual(createdUser)
})
it('should throw error for invalid email when creating user', async () => {
const userData = {
name: 'John Doe',
email: 'invalid-email',
role: 'user' as const
}
await expect(userService.createUser(userData)).rejects.toThrow('Invalid email format')
})
it('should update user role', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
role: 'user' as const
}
const user = await userService.createUser(userData)
const updatedUser = await userService.updateUserRole(user.id, 'admin')
expect(updatedUser.role).toBe('admin')
})
it('should delete user', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
role: 'user' as const
}
const user = await userService.createUser(userData)
const deleted = await userService.deleteUser(user.id)
expect(deleted).toBe(true)
await expect(userService.getUser(user.id)).rejects.toThrow('not found')
})
})
})
// 2. Performance Testing Example
// src/utils/performance.ts
export function fibonacci(n: number): number {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
export function memoizedFibonacci(): (n: number) => number {
const cache = new Map<number, number>()
return function fib(n: number): number {
if (cache.has(n)) return cache.get(n)!
if (n <= 1) return n
const result = fib(n - 1) + fib(n - 2)
cache.set(n, result)
return result
}
}
export async function processLargeArray<T>(array: T[], processor: (item: T) => Promise<T>): Promise<T[]> {
const promises = array.map(processor)
return Promise.all(promises)
}
// src/utils/performance.test.ts
import { describe, it, expect } from 'vitest'
import { fibonacci, memoizedFibonacci, processLargeArray } from './performance'
describe('Performance Tests', () => {
it('should complete fibonacci calculation within reasonable time', () => {
const start = performance.now()
const result = fibonacci(20)
const end = performance.now()
expect(result).toBe(6765)
expect(end - start).toBeLessThan(1000) // Should complete within 1 second
})
it('should show memoized version is significantly faster', () => {
const memFib = memoizedFibonacci()
const n = 40
// Non-memoized version
const start1 = performance.now()
const result1 = fibonacci(n)
const end1 = performance.now()
const time1 = end1 - start1
// Memoized version
const start2 = performance.now()
const result2 = memFib(n)
const end2 = performance.now()
const time2 = end2 - start2
expect(result1).toBe(result2)
expect(time2).toBeLessThan(time1 / 10) // Memoized should be at least 10x faster
})
it('should process large array efficiently', async () => {
const largeArray = Array.from({ length: 1000 }, (_, i) => i)
const processor = async (item: number): Promise<number> => {
await new Promise(resolve => setTimeout(resolve, 1))
return item * 2
}
const start = performance.now()
const result = await processLargeArray(largeArray, processor)
const end = performance.now()
expect(result).toHaveLength(1000)
expect(result[0]).toBe(0)
expect(result[999]).toBe(1998)
expect(end - start).toBeLessThan(5000) // Should complete within 5 seconds
})
})
// 3. Test Utilities and Fixtures
// src/test/utils/testUtils.ts
import { render, RenderOptions } from '@testing-library/react'
import { ReactElement } from 'react'
// Custom render function with providers
export const renderWithProviders = (
ui: ReactElement,
options: RenderOptions = {}
) => {
// Add any global providers here (Theme, Router, etc.)
return render(ui, {
...options,
})
}
// Test data factory
export class UserFactory {
static create(overrides: Partial<User> = {}): User {
return {
id: Math.floor(Math.random() * 1000),
name: 'Test User',
email: '[email protected]',
role: 'user',
...overrides
}
}
static createMany(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, () => this.create(overrides))
}
}
// Mock responses
export const createMockApiResponse = <T>(data: T, status = 200) => ({
data,
status,
headers: { 'content-type': 'application/json' }
})
// 4. API Testing Example
// src/services/api.ts
export interface ApiResponse<T> {
data: T
status: number
message: string
}
export class ApiClient {
constructor(private baseUrl: string) {}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return {
data,
status: response.status,
message: 'Success'
}
}
async post<T>(endpoint: string, body: any): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return {
data,
status: response.status,
message: 'Created'
}
}
}
// src/services/api.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { ApiClient } from './api'
describe('ApiClient', () => {
let apiClient: ApiClient
let mockFetch: ReturnType<typeof vi.fn>
beforeEach(() => {
mockFetch = vi.fn()
global.fetch = mockFetch
apiClient = new ApiClient('https://api.example.com')
})
it('should make GET request and return response', async () => {
const mockData = { id: 1, name: 'Test' }
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => mockData
})
const result = await apiClient.get('/users/1')
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1')
expect(result.data).toEqual(mockData)
expect(result.status).toBe(200)
expect(result.message).toBe('Success')
})
it('should handle HTTP errors', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404
})
await expect(apiClient.get('/users/999')).rejects.toThrow('HTTP error! status: 404')
})
it('should make POST request with body', async () => {
const requestBody = { name: 'New User' }
const responseData = { id: 2, ...requestBody }
mockFetch.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => responseData
})
const result = await apiClient.post('/users', requestBody)
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/users',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
}
)
expect(result.data).toEqual(responseData)
expect(result.status).toBe(201)
expect(result.message).toBe('Created')
})
})