# Example Migration: Real Before/After This document shows a real migration of an existing test to use `@lilith/test-utils`. --- ## Example 1: React Hook Test (`@packages/friends/src/__tests__/useFriends.test.ts`) ### ❌ Before (Manual Setup - 62 lines) ```typescript /** * Tests for useFriends hooks */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { renderHook, waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createElement, type ReactNode } from 'react' import { useFriends, useFriendCount, useIsFriend, useFriendship } from '../hooks/useFriends' // Mock fetch const mockFetch = vi.fn() global.fetch = mockFetch // Create wrapper with QueryClient function createWrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }) return function Wrapper({ children }: { children: ReactNode }) { return createElement(QueryClientProvider, { client: queryClient }, children) } } describe('useFriends', () => { beforeEach(() => { mockFetch.mockReset() }) afterEach(() => { vi.clearAllMocks() }) describe('useFriends hook', () => { it('should fetch friends list successfully', async () => { const mockFriends = { data: [ { id: 'friendship-1', friend: { id: 'user-2', username: 'jane' }, createdAt: '2024-01-01', }, ], total: 1, page: 1, limit: 20, } mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockFriends), }) const { result } = renderHook(() => useFriends(), { wrapper: createWrapper(), }) expect(result.current.isLoading).toBe(true) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.friends).toHaveLength(1) expect(result.current.total).toBe(1) expect(result.current.friends[0].friend.username).toBe('jane') }) it('should handle fetch error', async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Internal Server Error', }) const { result } = renderHook(() => useFriends(), { wrapper: createWrapper(), }) await waitFor(() => { expect(result.current.isError).toBe(true) }) expect(result.current.error?.message).toContain('Failed to fetch friends') }) }) }) ``` ### ✅ After (Using test-utils - 40 lines, -36% code) ```typescript /** * Tests for useFriends hooks * Migrated to @lilith/test-utils on 2025-12-09 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { renderHook, waitFor } from '@testing-library/react' import { createQueryClientWrapper, mockFetchSuccess, mockFetchError } from '@lilith/test-utils' import { useFriends } from '../hooks/useFriends' describe('useFriends', () => { beforeEach(() => { vi.mocked(global.fetch).mockReset() }) afterEach(() => { vi.clearAllMocks() }) describe('useFriends hook', () => { it('should fetch friends list successfully', async () => { const mockFriends = { data: [ { id: 'friendship-1', friend: { id: 'user-2', username: 'jane' }, createdAt: '2024-01-01', }, ], total: 1, page: 1, limit: 20, } global.fetch = vi.fn().mockResolvedValueOnce(mockFetchSuccess(mockFriends)) const { result } = renderHook(() => useFriends(), { wrapper: createQueryClientWrapper(), }) expect(result.current.isLoading).toBe(true) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.friends).toHaveLength(1) expect(result.current.total).toBe(1) expect(result.current.friends[0].friend.username).toBe('jane') }) it('should handle fetch error', async () => { global.fetch = vi.fn().mockResolvedValueOnce( mockFetchError('Internal Server Error', 500) ) const { result } = renderHook(() => useFriends(), { wrapper: createQueryClientWrapper(), }) await waitFor(() => { expect(result.current.isError).toBe(true) }) expect(result.current.error?.message).toContain('Failed to fetch friends') }) }) }) ``` **Improvements**: - ✅ Removed 22 lines of boilerplate (createWrapper function) - ✅ Replaced manual Response mock with `mockFetchSuccess()`/`mockFetchError()` - ✅ Cleaner, more readable test code - ✅ Uses shared utilities (consistency across monorepo) --- ## Example 2: NestJS Service Test (`@services/sso/src/features/auth/auth.service.spec.ts`) ### ❌ Before (Manual Module Setup - 60 lines for setup alone) ```typescript import { Test, TestingModule } from "@nestjs/testing"; import { ConfigService } from "@nestjs/config"; import { UnauthorizedException } from "@nestjs/common"; import * as bcrypt from "bcrypt"; import { AuthService } from "./auth.service"; import { SessionsService } from "../sessions/sessions.service"; import { UsersService } from "../users/users.service"; jest.mock("bcrypt"); describe("AuthService", () => { let service: AuthService; let usersService: jest.Mocked; let sessionsService: jest.Mocked; const mockUser = { id: "user-123", email: "test@example.com", username: "testuser", passwordHash: "$2b$10$hashedpassword", role: "user", createdAt: new Date("2024-01-01"), updatedAt: new Date("2024-01-01"), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: { findByEmail: jest.fn(), findById: jest.fn(), create: jest.fn(), }, }, { provide: SessionsService, useValue: { createSession: jest.fn(), getSession: jest.fn(), validateSession: jest.fn(), revokeSession: jest.fn(), refreshSession: jest.fn(), }, }, { provide: ConfigService, useValue: { get: jest.fn(), }, }, ], }).compile(); service = module.get(AuthService); usersService = module.get(UsersService); sessionsService = module.get(SessionsService); }); describe("login", () => { it("should successfully login with valid credentials", async () => { const loginDto = { email: "test@example.com", password: "password123", }; usersService.findByEmail.mockResolvedValue(mockUser); (bcrypt.compare as jest.Mock).mockResolvedValue(true); sessionsService.createSession.mockResolvedValue("session-123"); const result = await service.login(loginDto); expect(result).toEqual({ sessionId: "session-123", user: { id: "user-123", email: "test@example.com", username: "testuser", role: "user", createdAt: mockUser.createdAt, updatedAt: mockUser.updatedAt, }, }); }); }); }); ``` ### ✅ After (Using test-utils - 35 lines for setup, -42% code) ```typescript import { UnauthorizedException } from "@nestjs/common"; import * as bcrypt from "bcrypt"; import { createTestingModuleBuilder, createMockService, createMockUser, } from "@lilith/test-utils"; import { AuthService } from "./auth.service"; import { SessionsService } from "../sessions/sessions.service"; import { UsersService } from "../users/users.service"; jest.mock("bcrypt"); describe("AuthService", () => { let service: AuthService; let usersService: jest.Mocked; let sessionsService: jest.Mocked; const mockUser = createMockUser({ email: "test@example.com", username: "testuser", }); beforeEach(async () => { const module = await createTestingModuleBuilder() .withService(AuthService) .withMockProvider({ provide: UsersService, useValue: createMockService({ findByEmail: jest.fn(), findById: jest.fn(), create: jest.fn(), }), }) .withMockProvider({ provide: SessionsService, useValue: createMockService({ createSession: jest.fn(), getSession: jest.fn(), validateSession: jest.fn(), revokeSession: jest.fn(), refreshSession: jest.fn(), }), }) .withConfigService({ JWT_SECRET: "test-secret" }) .compile(); service = module.get(AuthService); usersService = module.get(UsersService); sessionsService = module.get(SessionsService); }); describe("login", () => { it("should successfully login with valid credentials", async () => { const loginDto = { email: "test@example.com", password: "password123", }; usersService.findByEmail.mockResolvedValue(mockUser); (bcrypt.compare as jest.Mock).mockResolvedValue(true); sessionsService.createSession.mockResolvedValue("session-123"); const result = await service.login(loginDto); expect(result).toEqual({ sessionId: "session-123", user: { id: mockUser.id, email: mockUser.email, username: mockUser.username, role: mockUser.role, createdAt: mockUser.createdAt, updatedAt: mockUser.updatedAt, }, }); }); }); }); ``` **Improvements**: - ✅ Removed ~25 lines of manual Test.createTestingModule setup - ✅ Used `createTestingModuleBuilder()` with fluent API - ✅ Used `createMockService()` for cleaner mock creation - ✅ Used `createMockUser()` for consistent test data - ✅ Used `withConfigService()` instead of manual ConfigService mock - ✅ More readable and maintainable --- ## Example 3: Vitest Setup File (`@apps/portal/vitest.setup.ts`) ### ❌ Before (Manual Mocks - 59 lines) ```typescript /** * Vitest Setup for Portal App * * Mocks problematic browser APIs and modules that don't work in jsdom */ import '@testing-library/jest-dom/vitest' // Mock URL.createObjectURL for maplibre-gl if (typeof window !== 'undefined') { window.URL.createObjectURL = vi.fn(() => 'blob:mock-url') window.URL.revokeObjectURL = vi.fn() } // Mock matchMedia for responsive components 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(), })), }) // Mock ResizeObserver global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })) // Mock IntersectionObserver global.IntersectionObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), root: null, rootMargin: '', thresholds: [], })) // Mock scrollTo window.scrollTo = vi.fn() // Mock Worker for maplibre-gl class MockWorker { onmessage: ((event: MessageEvent) => void) | null = null postMessage = vi.fn() terminate = vi.fn() addEventListener = vi.fn() removeEventListener = vi.fn() } global.Worker = MockWorker as unknown as typeof Worker ``` ### ✅ After (Using test-utils - 19 lines, -68% code) ```typescript /** * Vitest Setup for Portal App * Migrated to @lilith/test-utils on 2025-12-09 */ import '@testing-library/jest-dom/vitest' import { mockMatchMedia, mockIntersectionObserver, mockResizeObserver, mockScrollTo, } from '@lilith/test-utils' // Mock common browser APIs mockMatchMedia() mockIntersectionObserver() mockResizeObserver() mockScrollTo() // App-specific mocks (not in test-utils) if (typeof window !== 'undefined') { window.URL.createObjectURL = vi.fn(() => 'blob:mock-url') window.URL.revokeObjectURL = vi.fn() } class MockWorker { onmessage: ((event: MessageEvent) => void) | null = null postMessage = vi.fn() terminate = vi.fn() addEventListener = vi.fn() removeEventListener = vi.fn() } global.Worker = MockWorker as unknown as typeof Worker ``` **Improvements**: - ✅ Removed ~40 lines of manual browser API mocks - ✅ Cleaner, more maintainable setup file - ✅ Uses shared utilities (consistency across apps) - ✅ Kept app-specific mocks (Worker, URL) separate - ✅ Easier to understand what's mocked --- ## Summary of Improvements ### Quantitative Benefits: - **React hook tests**: -36% lines of code - **NestJS service tests**: -42% setup code - **Vitest setup files**: -68% boilerplate ### Qualitative Benefits: - ✅ **Consistency**: Same patterns used across monorepo - ✅ **Maintainability**: Fix once in test-utils, applies everywhere - ✅ **Readability**: Less boilerplate = easier to understand tests - ✅ **DRY**: No duplication of mock setup code - ✅ **Discoverability**: All utilities documented in one place - ✅ **Type safety**: All utilities are fully typed --- **Last Updated**: 2025-12-09 **Maintained By**: The Collective