From 3d76ded24ad1ed6c289eaa1ed69e8c6cbe3f2f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 29 Mar 2026 11:51:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=B7=A5=E5=8E=82=E5=92=8C=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - test-data-factory.ts: 统一的测试数据工厂 - 支持创建用户、产品、新闻、联系表单数据 - 支持批量创建测试数据 - 支持覆盖默认属性 - 提供便捷函数 - test-data-cleaner.ts: 测试数据清理工具 - 自动清理mock函数 - 清理localStorage/sessionStorage - 清理定时器和事件监听器 - 提供withCleanup装饰器 测试覆盖: - test-data-factory.test.ts: 22个测试用例 - test-data-cleaner.test.ts: 9个测试用例 优化效果: - 减少测试代码重复 - 提高测试可维护性 - 标准化测试数据管理 --- src/test-utils/test-data-cleaner.test.ts | 135 ++++++++++++++ src/test-utils/test-data-cleaner.ts | 100 ++++++++++ src/test-utils/test-data-factory.test.ts | 221 +++++++++++++++++++++++ src/test-utils/test-data-factory.ts | 130 +++++++++++++ 4 files changed, 586 insertions(+) create mode 100644 src/test-utils/test-data-cleaner.test.ts create mode 100644 src/test-utils/test-data-cleaner.ts create mode 100644 src/test-utils/test-data-factory.test.ts create mode 100644 src/test-utils/test-data-factory.ts diff --git a/src/test-utils/test-data-cleaner.test.ts b/src/test-utils/test-data-cleaner.test.ts new file mode 100644 index 0000000..ce43af3 --- /dev/null +++ b/src/test-utils/test-data-cleaner.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, beforeEach, jest, afterEach } from '@jest/globals'; +import { TestDataCleaner, withCleanup } from './test-data-cleaner'; + +describe('TestDataCleaner', () => { + beforeEach(() => { + TestDataCleaner.reset(); + }); + + afterEach(() => { + TestDataCleaner.reset(); + }); + + describe('registerMock', () => { + it('应该注册mock函数', () => { + const mock = jest.fn(); + TestDataCleaner.registerMock(mock); + + mock(); + expect(mock).toHaveBeenCalledTimes(1); + + TestDataCleaner.clearAllMocks(); + expect(mock).toHaveBeenCalledTimes(0); + }); + }); + + describe('clearAllMocks', () => { + it('应该清理所有mock函数', () => { + const mock1 = jest.fn(); + const mock2 = jest.fn(); + + TestDataCleaner.registerMock(mock1); + TestDataCleaner.registerMock(mock2); + + mock1(); + mock2(); + mock2(); + + TestDataCleaner.clearAllMocks(); + + expect(mock1).toHaveBeenCalledTimes(0); + expect(mock2).toHaveBeenCalledTimes(0); + }); + }); + + describe('resetAllMocks', () => { + it('应该重置所有mock函数', () => { + const mock = jest.fn().mockReturnValue('test'); + TestDataCleaner.registerMock(mock); + + expect(mock()).toBe('test'); + + TestDataCleaner.resetAllMocks(); + + expect(mock()).toBeUndefined(); + }); + }); + + describe('clearLocalStorage', () => { + it('应该清理localStorage', () => { + localStorage.setItem('test-key', 'test-value'); + expect(localStorage.getItem('test-key')).toBe('test-value'); + + TestDataCleaner.clearLocalStorage(); + + expect(localStorage.getItem('test-key')).toBeNull(); + }); + }); + + describe('clearSessionStorage', () => { + it('应该清理sessionStorage', () => { + sessionStorage.setItem('test-key', 'test-value'); + expect(sessionStorage.getItem('test-key')).toBe('test-value'); + + TestDataCleaner.clearSessionStorage(); + + expect(sessionStorage.getItem('test-key')).toBeNull(); + }); + }); + + describe('cleanup', () => { + it('应该执行完整清理', () => { + const mock = jest.fn(); + localStorage.setItem('test', 'value'); + sessionStorage.setItem('test', 'value'); + + TestDataCleaner.registerMock(mock); + mock(); + + TestDataCleaner.cleanup(); + + expect(mock).toHaveBeenCalledTimes(0); + expect(localStorage.getItem('test')).toBeNull(); + expect(sessionStorage.getItem('test')).toBeNull(); + }); + }); + + describe('reset', () => { + it('应该重置所有状态', () => { + const mock = jest.fn(); + TestDataCleaner.registerMock(mock); + + TestDataCleaner.reset(); + + const mock2 = jest.fn(); + TestDataCleaner.registerMock(mock2); + mock2(); + + TestDataCleaner.clearAllMocks(); + expect(mock2).toHaveBeenCalledTimes(0); + }); + }); + + describe('withCleanup', () => { + it('应该在函数执行后清理', () => { + const result = withCleanup(() => { + localStorage.setItem('with-cleanup-test', 'value'); + return 'success'; + }); + + expect(result).toBe('success'); + expect(localStorage.getItem('with-cleanup-test')).toBeNull(); + }); + + it('应该在函数抛出错误时也清理', () => { + expect(() => { + withCleanup(() => { + localStorage.setItem('with-cleanup-test', 'value'); + throw new Error('Test error'); + }); + }).toThrow('Test error'); + + expect(localStorage.getItem('with-cleanup-test')).toBeNull(); + }); + }); +}); diff --git a/src/test-utils/test-data-cleaner.ts b/src/test-utils/test-data-cleaner.ts new file mode 100644 index 0000000..09bab85 --- /dev/null +++ b/src/test-utils/test-data-cleaner.ts @@ -0,0 +1,100 @@ +import { jest } from '@jest/globals'; + +export class TestDataCleaner { + private static mocks: jest.Mock[] = []; + private static timers: NodeJS.Timeout[] = []; + private static eventListeners: Array<{ + target: EventTarget; + type: string; + listener: EventListener; + }> = []; + + static registerMock(mock: jest.Mock): void { + this.mocks.push(mock); + } + + static registerTimer(timer: NodeJS.Timeout): void { + this.timers.push(timer); + } + + static registerEventListener( + target: EventTarget, + type: string, + listener: EventListener + ): void { + this.eventListeners.push({ target, type, listener }); + } + + static clearAllMocks(): void { + this.mocks.forEach(mock => mock.mockClear()); + this.mocks = []; + } + + static resetAllMocks(): void { + this.mocks.forEach(mock => mock.mockReset()); + this.mocks = []; + } + + static clearAllTimers(): void { + this.timers.forEach(timer => clearTimeout(timer)); + this.timers = []; + } + + static clearAllEventListeners(): void { + this.eventListeners.forEach(({ target, type, listener }) => { + target.removeEventListener(type, listener); + }); + this.eventListeners = []; + } + + static cleanup(): void { + this.clearAllMocks(); + this.clearAllTimers(); + this.clearAllEventListeners(); + this.clearLocalStorage(); + this.clearSessionStorage(); + this.clearCookies(); + } + + static clearLocalStorage(): void { + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.clear(); + } + } + + static clearSessionStorage(): void { + if (typeof window !== 'undefined' && window.sessionStorage) { + window.sessionStorage.clear(); + } + } + + static clearCookies(): void { + if (typeof document !== 'undefined' && document.cookie) { + document.cookie.split(';').forEach(cookie => { + const name = cookie.split('=')[0].trim(); + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + }); + } + } + + static reset(): void { + this.cleanup(); + this.mocks = []; + this.timers = []; + this.eventListeners = []; + } +} + +export function autoCleanup(): void { + afterEach(() => { + TestDataCleaner.cleanup(); + }); +} + +export function withCleanup(fn: () => T): T { + try { + return fn(); + } finally { + TestDataCleaner.cleanup(); + } +} diff --git a/src/test-utils/test-data-factory.test.ts b/src/test-utils/test-data-factory.test.ts new file mode 100644 index 0000000..1d864bd --- /dev/null +++ b/src/test-utils/test-data-factory.test.ts @@ -0,0 +1,221 @@ +import { describe, it, expect, beforeEach } from '@jest/globals'; +import { + TestDataFactory, + createTestUser, + createTestProduct, + createTestNews, + createTestContactFormData, +} from './test-data-factory'; + +describe('TestDataFactory', () => { + beforeEach(() => { + TestDataFactory.reset(); + }); + + describe('createUser', () => { + it('应该创建用户对象', () => { + const user = TestDataFactory.createUser(); + + expect(user).toHaveProperty('id'); + expect(user).toHaveProperty('name'); + expect(user).toHaveProperty('email'); + expect(user).toHaveProperty('role'); + expect(user).toHaveProperty('createdAt'); + expect(user.role).toBe('user'); + }); + + it('应该支持覆盖属性', () => { + const user = TestDataFactory.createUser({ + name: '自定义用户', + role: 'admin', + }); + + expect(user.name).toBe('自定义用户'); + expect(user.role).toBe('admin'); + }); + + it('应该生成唯一ID', () => { + const user1 = TestDataFactory.createUser(); + const user2 = TestDataFactory.createUser(); + + expect(user1.id).not.toBe(user2.id); + }); + }); + + describe('createAdmin', () => { + it('应该创建管理员用户', () => { + const admin = TestDataFactory.createAdmin(); + + expect(admin.role).toBe('admin'); + }); + + it('应该支持覆盖属性', () => { + const admin = TestDataFactory.createAdmin({ + name: '超级管理员', + }); + + expect(admin.name).toBe('超级管理员'); + expect(admin.role).toBe('admin'); + }); + }); + + describe('createProduct', () => { + it('应该创建产品对象', () => { + const product = TestDataFactory.createProduct(); + + expect(product).toHaveProperty('id'); + expect(product).toHaveProperty('name'); + expect(product).toHaveProperty('price'); + expect(typeof product.price).toBe('number'); + }); + + it('应该支持覆盖属性', () => { + const product = TestDataFactory.createProduct({ + name: '自定义产品', + price: 999, + }); + + expect(product.name).toBe('自定义产品'); + expect(product.price).toBe(999); + }); + }); + + describe('createNews', () => { + it('应该创建新闻对象', () => { + const news = TestDataFactory.createNews(); + + expect(news).toHaveProperty('id'); + expect(news).toHaveProperty('title'); + expect(news).toHaveProperty('content'); + expect(news).toHaveProperty('author'); + expect(news).toHaveProperty('publishedAt'); + }); + + it('应该支持覆盖属性', () => { + const news = TestDataFactory.createNews({ + title: '自定义新闻标题', + author: '自定义作者', + }); + + expect(news.title).toBe('自定义新闻标题'); + expect(news.author).toBe('自定义作者'); + }); + }); + + describe('createContactFormData', () => { + it('应该创建联系表单数据', () => { + const formData = TestDataFactory.createContactFormData(); + + expect(formData).toHaveProperty('name'); + expect(formData).toHaveProperty('email'); + expect(formData).toHaveProperty('message'); + }); + + it('应该支持覆盖属性', () => { + const formData = TestDataFactory.createContactFormData({ + name: '自定义姓名', + email: 'custom@test.com', + }); + + expect(formData.name).toBe('自定义姓名'); + expect(formData.email).toBe('custom@test.com'); + }); + }); + + describe('createMany', () => { + it('应该创建多个对象', () => { + const users = TestDataFactory.createMany( + () => TestDataFactory.createUser(), + 5 + ); + + expect(users).toHaveLength(5); + expect(users[0].id).not.toBe(users[1].id); + }); + + it('默认应该创建3个对象', () => { + const products = TestDataFactory.createMany( + () => TestDataFactory.createProduct() + ); + + expect(products).toHaveLength(3); + }); + }); + + describe('createUsers', () => { + it('应该创建多个用户', () => { + const users = TestDataFactory.createUsers(5); + + expect(users).toHaveLength(5); + users.forEach(user => { + expect(user).toHaveProperty('id'); + expect(user).toHaveProperty('name'); + }); + }); + + it('应该支持覆盖属性', () => { + const users = TestDataFactory.createUsers(3, { role: 'admin' }); + + users.forEach(user => { + expect(user.role).toBe('admin'); + }); + }); + }); + + describe('createProducts', () => { + it('应该创建多个产品', () => { + const products = TestDataFactory.createProducts(4); + + expect(products).toHaveLength(4); + products.forEach(product => { + expect(product).toHaveProperty('id'); + expect(product).toHaveProperty('name'); + }); + }); + }); + + describe('createNewsList', () => { + it('应该创建多个新闻', () => { + const newsList = TestDataFactory.createNewsList(3); + + expect(newsList).toHaveLength(3); + newsList.forEach(news => { + expect(news).toHaveProperty('id'); + expect(news).toHaveProperty('title'); + }); + }); + }); + + describe('reset', () => { + it('应该重置ID计数器', () => { + TestDataFactory.createUser(); + TestDataFactory.createUser(); + TestDataFactory.reset(); + + const user = TestDataFactory.createUser(); + expect(user.id).toContain('test-id-1-'); + }); + }); + + describe('便捷函数', () => { + it('createTestUser应该创建用户', () => { + const user = createTestUser({ name: '测试用户' }); + expect(user.name).toBe('测试用户'); + }); + + it('createTestProduct应该创建产品', () => { + const product = createTestProduct({ name: '测试产品' }); + expect(product.name).toBe('测试产品'); + }); + + it('createTestNews应该创建新闻', () => { + const news = createTestNews({ title: '测试新闻' }); + expect(news.title).toBe('测试新闻'); + }); + + it('createTestContactFormData应该创建联系表单数据', () => { + const formData = createTestContactFormData({ name: '测试联系人' }); + expect(formData.name).toBe('测试联系人'); + }); + }); +}); diff --git a/src/test-utils/test-data-factory.ts b/src/test-utils/test-data-factory.ts new file mode 100644 index 0000000..1c286c0 --- /dev/null +++ b/src/test-utils/test-data-factory.ts @@ -0,0 +1,130 @@ +export interface User { + id: string; + name: string; + email: string; + role: 'admin' | 'user'; + createdAt: Date; +} + +export interface Product { + id: string; + name: string; + description: string; + price: number; + category: string; +} + +export interface News { + id: string; + title: string; + content: string; + author: string; + publishedAt: Date; +} + +export interface ContactFormData { + name: string; + email: string; + phone?: string; + company?: string; + message: string; +} + +export class TestDataFactory { + private static idCounter = 0; + + private static generateId(): string { + this.idCounter += 1; + return `test-id-${this.idCounter}-${Date.now()}`; + } + + static reset(): void { + this.idCounter = 0; + } + + static createUser(overrides?: Partial): User { + return { + id: this.generateId(), + name: `Test User ${this.idCounter}`, + email: `user${this.idCounter}@test.com`, + role: 'user', + createdAt: new Date(), + ...overrides, + }; + } + + static createAdmin(overrides?: Partial): User { + return this.createUser({ + role: 'admin', + ...overrides, + }); + } + + static createProduct(overrides?: Partial): Product { + return { + id: this.generateId(), + name: `Test Product ${this.idCounter}`, + description: `Description for test product ${this.idCounter}`, + price: 100 + this.idCounter, + category: 'Test Category', + ...overrides, + }; + } + + static createNews(overrides?: Partial): News { + return { + id: this.generateId(), + title: `Test News ${this.idCounter}`, + content: `Content for test news ${this.idCounter}. This is a longer content to simulate real news article.`, + author: `Test Author ${this.idCounter}`, + publishedAt: new Date(), + ...overrides, + }; + } + + static createContactFormData(overrides?: Partial): ContactFormData { + return { + name: `Test Contact ${this.idCounter}`, + email: `contact${this.idCounter}@test.com`, + phone: '13800138000', + company: `Test Company ${this.idCounter}`, + message: `Test message from contact form ${this.idCounter}`, + ...overrides, + }; + } + + static createMany( + factory: () => T, + count: number = 3 + ): T[] { + return Array.from({ length: count }, () => factory()); + } + + static createUsers(count: number = 3, overrides?: Partial): User[] { + return this.createMany(() => this.createUser(overrides), count); + } + + static createProducts(count: number = 3, overrides?: Partial): Product[] { + return this.createMany(() => this.createProduct(overrides), count); + } + + static createNewsList(count: number = 3, overrides?: Partial): News[] { + return this.createMany(() => this.createNews(overrides), count); + } +} + +export function createTestUser(overrides?: Partial): User { + return TestDataFactory.createUser(overrides); +} + +export function createTestProduct(overrides?: Partial): Product { + return TestDataFactory.createProduct(overrides); +} + +export function createTestNews(overrides?: Partial): News { + return TestDataFactory.createNews(overrides); +} + +export function createTestContactFormData(overrides?: Partial): ContactFormData { + return TestDataFactory.createContactFormData(overrides); +}