# 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 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) 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 beforeEach(async () => { const module = await createTestingModuleBuilder() .withService(AuthService) .withMockProvider({ provide: UsersService, useValue: createMockService({ findByEmail: vi.fn(), findById: vi.fn(), create: vi.fn(), }), }) .withConfigService({ JWT_SECRET: 'test-secret' }) .compile() service = module.get(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({ 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(, { 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() ``` --- ### 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