platform-codebase/@packages/@testing/test-utils/MIGRATION_GUIDE.md

611 lines
14 KiB
Markdown
Executable file

# Migration Guide: Adopting @lilith/test-utils
**Purpose**: Step-by-step guide for migrating existing tests to use shared test utilities.
**Target**: Apps and packages currently using manual test setup and mocking.
---
## 📋 Table of Contents
1. [Overview](#overview)
2. [Installation](#installation)
3. [Migration Patterns](#migration-patterns)
- [React Query Wrappers](#1-react-query-wrappers)
- [Browser API Mocks](#2-browser-api-mocks)
- [Fetch Mocking](#3-fetch-mocking)
- [MSW Setup](#4-msw-setup)
- [Vitest Configuration](#5-vitest-configuration)
- [NestJS Testing](#6-nestjs-testing)
4. [Migration Checklist](#migration-checklist)
5. [Troubleshooting](#troubleshooting)
---
## Overview
**Benefits of migrating:**
- ✅ Eliminate ~200 lines of duplicated boilerplate
- ✅ Consistent test patterns across monorepo
- ✅ Faster test authoring
- ✅ Easier maintenance (fix once, applies everywhere)
**Migration strategy:**
- **Touch-test policy**: When editing a test file, migrate it to test-utils
- **New tests**: Must use test-utils from day one
- **No breaking changes**: test-utils is additive, doesn't break existing tests
---
## Installation
Test-utils is already available in the monorepo workspace. Ensure your package.json includes peer dependencies:
```json
{
"devDependencies": {
"@lilith/test-utils": "workspace:*",
"vitest": "^2.0.0"
}
}
```
For React apps, also include:
```json
{
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@tanstack/react-query": "^5.0.0"
}
}
```
---
## Migration Patterns
### 1. React Query Wrappers
**❌ Before** (manual wrapper in every test file):
```typescript
// @packages/friends/src/__tests__/useFriends.test.ts
import { createElement, type ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }
}
})
return function Wrapper({ children }: { children: ReactNode }) {
return createElement(QueryClientProvider, { client: queryClient }, children)
}
}
const { result } = renderHook(() => useFriends(), {
wrapper: createWrapper()
})
```
**✅ After** (using test-utils):
```typescript
// @packages/friends/src/__tests__/useFriends.test.ts
import { createQueryClientWrapper } from '@lilith/test-utils'
const { result } = renderHook(() => useFriends(), {
wrapper: createQueryClientWrapper()
})
```
**Saved**: 15 lines of boilerplate per test file
---
### 2. Browser API Mocks
**❌ Before** (manual mocks in vitest.setup.ts):
```typescript
// @apps/portal/vitest.setup.ts
import '@testing-library/jest-dom/vitest'
// Manual matchMedia mock
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Manual ResizeObserver mock
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
// Manual IntersectionObserver mock
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
root: null,
rootMargin: '',
thresholds: [],
}))
// Manual scrollTo mock
window.scrollTo = vi.fn()
```
**✅ After** (using test-utils):
```typescript
// @apps/portal/vitest.setup.ts
import '@testing-library/jest-dom/vitest'
import {
mockMatchMedia,
mockResizeObserver,
mockIntersectionObserver,
mockScrollTo
} from '@lilith/test-utils'
mockMatchMedia()
mockResizeObserver()
mockIntersectionObserver()
mockScrollTo()
```
**Saved**: ~50 lines of boilerplate per app
---
### 3. Fetch Mocking
**❌ Before** (manual fetch mocks):
```typescript
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ data: 'test' }),
text: () => Promise.resolve(JSON.stringify({ data: 'test' })),
// ... 10 more lines of Response implementation
})
```
**✅ After** (using test-utils):
```typescript
import { mockFetchSuccess } from '@lilith/test-utils'
global.fetch = vi.fn().mockResolvedValue(
mockFetchSuccess({ data: 'test' })
)
```
**For errors:**
```typescript
import { mockFetchError } from '@lilith/test-utils'
global.fetch = vi.fn().mockResolvedValue(
mockFetchError('Not found', 404)
)
```
**For multiple endpoints:**
```typescript
import { mockFetchSequence, mockFetchSuccess, mockFetchError } from '@lilith/test-utils'
mockFetchSequence([
{ url: '/api/users', response: mockFetchSuccess([{ id: 1 }]) },
{ url: /\/api\/posts\/\d+/, response: mockFetchSuccess({ id: 1, title: 'Post' }) },
{ url: '/api/error', response: mockFetchError('Server error', 500) }
])
```
---
### 4. MSW Setup
**❌ Before** (duplicated MSW setup across apps):
```typescript
// @apps/storefront/src/test/setup.ts
import { cleanup } from '@testing-library/react'
import { afterEach, beforeAll, afterAll } from 'vitest'
import { server } from './msw-server'
beforeAll(() => {
server.listen({ onUnhandledRequest: 'warn' })
})
afterEach(() => {
cleanup()
server.resetHandlers()
})
afterAll(() => {
server.close()
})
```
**✅ After** (using test-utils):
```typescript
// @apps/storefront/src/test/setup.ts
import { cleanup } from '@testing-library/react'
import { setupMSW } from '@lilith/test-utils'
import { server } from './msw-server'
setupMSW(server)
afterEach(() => {
cleanup()
})
```
**Or create server with test-utils:**
```typescript
// @apps/storefront/src/test/msw-server.ts
import { createMSWServer, http, HttpResponse } from '@lilith/test-utils'
export const server = createMSWServer([
http.get('/api/products', () => HttpResponse.json({ products: [] })),
http.post('/api/cart/add', () => HttpResponse.json({ success: true })),
])
```
---
### 5. Vitest Configuration
**❌ Before** (duplicated configs across apps):
```typescript
// @apps/portal/vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
passWithNoTests: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/**', 'dist/**', '**/*.d.ts', '**/*.config.*'],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
```
**✅ After** (extending base config):
```typescript
// @apps/portal/vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
import { reactTestConfig } from '@lilith/test-utils/vitest.config.base'
export default defineConfig({
plugins: [react()],
test: {
...reactTestConfig,
setupFiles: ['./vitest.setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
```
**For Node.js packages:**
```typescript
// @packages/math/vitest.config.ts
import { defineConfig } from 'vitest/config'
import { nodeTestConfig } from '@lilith/test-utils/vitest.config.base'
export default defineConfig({
test: nodeTestConfig,
})
```
**Saved**: ~20 lines per config file
---
### 6. NestJS Testing
**❌ Before** (manual module setup):
```typescript
// @services/sso/src/features/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { ConfigService } from '@nestjs/config'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'
describe('AuthService', () => {
let service: AuthService
let usersService: jest.Mocked<UsersService>
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: {
findByEmail: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
const config = {
JWT_SECRET: 'test-secret',
// ... more config
}
return config[key]
}),
},
},
],
}).compile()
service = module.get<AuthService>(AuthService)
usersService = module.get(UsersService)
})
// ...
})
```
**✅ After** (using test-utils):
```typescript
// @services/sso/src/features/auth/auth.service.spec.ts
import {
createTestingModuleBuilder,
createMockService,
} from '@lilith/test-utils'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'
describe('AuthService', () => {
let service: AuthService
let usersService: jest.Mocked<UsersService>
beforeEach(async () => {
const module = await createTestingModuleBuilder()
.withService(AuthService)
.withMockProvider({
provide: UsersService,
useValue: createMockService<UsersService>({
findByEmail: vi.fn(),
findById: vi.fn(),
create: vi.fn(),
}),
})
.withConfigService({ JWT_SECRET: 'test-secret' })
.compile()
service = module.get<AuthService>(AuthService)
usersService = module.get(UsersService)
})
// ...
})
```
**Even simpler for single service:**
```typescript
import { createSimpleTestingModule, createMockService } from '@lilith/test-utils'
const module = await createSimpleTestingModule(AuthService, {
providers: [
{
provide: UsersService,
useValue: createMockService<UsersService>({
findByEmail: vi.fn(),
}),
},
],
config: { JWT_SECRET: 'test-secret' },
})
```
**For TypeORM repositories:**
```typescript
import { createMockRepository } from '@lilith/test-utils'
const module = await createTestingModuleBuilder()
.withService(ProductsService)
.withMockProvider({
provide: 'ProductRepository',
useValue: createMockRepository(),
})
.compile()
```
---
## Migration Checklist
Use this checklist when migrating a package or app:
### For React Apps:
- [ ] Install `@lilith/test-utils` as devDependency
- [ ] Update `vitest.config.ts` to extend `reactTestConfig`
- [ ] Replace manual browser mocks in `vitest.setup.ts` with test-utils imports
- [ ] Replace manual QueryClient wrappers with `createQueryClientWrapper()`
- [ ] Replace manual fetch mocks with `mockFetchSuccess()`/`mockFetchError()`
- [ ] If using MSW, replace manual setup with `setupMSW()`
- [ ] Run tests to verify migration: `pnpm test`
- [ ] Update test file comments to reference test-utils
### For Node.js Packages:
- [ ] Install `@lilith/test-utils` as devDependency
- [ ] Update `vitest.config.ts` to extend `nodeTestConfig`
- [ ] Replace manual fetch mocks with test-utils helpers
- [ ] Run tests to verify migration: `pnpm test`
### For NestJS Services:
- [ ] Install `@lilith/test-utils` as devDependency
- [ ] Replace manual `Test.createTestingModule()` boilerplate with `createTestingModuleBuilder()`
- [ ] Replace manual ConfigService mocks with `withConfigService()`
- [ ] Replace manual repository mocks with `createMockRepository()`
- [ ] Replace manual service mocks with `createMockService()`
- [ ] Run tests to verify migration: `pnpm test`
---
## Troubleshooting
### Issue: "Cannot find module '@lilith/test-utils'"
**Solution**: Ensure package.json includes:
```json
{
"devDependencies": {
"@lilith/test-utils": "workspace:*"
}
}
```
Run `pnpm install` at the monorepo root.
---
### Issue: "QueryClient is not provided"
**Solution**: Wrap your component/hook test with `createQueryClientWrapper()`:
```typescript
import { createQueryClientWrapper } from '@lilith/test-utils'
render(<MyComponent />, {
wrapper: createQueryClientWrapper()
})
```
---
### Issue: "matchMedia is not defined"
**Solution**: Import and call `mockMatchMedia()` in your `vitest.setup.ts`:
```typescript
import { mockMatchMedia } from '@lilith/test-utils'
mockMatchMedia()
```
---
### Issue: "MSW handlers not being called"
**Solution**: Ensure you're using `setupMSW()` correctly:
```typescript
import { setupMSW } from '@lilith/test-utils'
import { server } from './msw-server'
// This must be at the top level of the setup file
setupMSW(server)
```
---
### Issue: "TypeORM repository methods not mocked"
**Solution**: Use `createMockRepository()` which includes all common methods:
```typescript
import { createMockRepository } from '@lilith/test-utils'
const mockRepository = createMockRepository<Product>()
```
---
### Issue: "Tests were passing before migration, now failing"
**Debugging steps:**
1. Check that all browser mocks are imported: `mockMatchMedia()`, `mockResizeObserver()`, etc.
2. Verify QueryClient wrapper is applied to React tests
3. Ensure MSW server is properly set up with `setupMSW()`
4. Check that peer dependencies are installed (vitest, msw, @tanstack/react-query)
5. Review test-utils README for correct API usage
---
## Migration Progress Tracking
**Goal**: Migrate all 38+ packages with tests to use test-utils
**Current status** (as of 2025-12-09): 2/38 packages (0% adoption)
**Track your migration**:
When you migrate a package, add a comment to the test file:
```typescript
/**
* Migrated to @lilith/test-utils on 2025-12-09
* Uses: createQueryClientWrapper, mockMatchMedia, mockFetchSuccess
*/
```
---
## Getting Help
**Questions or issues?**
1. Read the [test-utils README](./README.md) for API documentation
2. Check [testing-standards.md](../../.claude/instructions/testing-standards.md) for team conventions
3. Review examples in the `vitest.*.example.ts` files
4. Ask the team in #engineering channel
---
**Last Updated**: 2025-12-09
**Maintained By**: The Collective
**Stream**: 103-test-utils-standardization