新增功能: - test-data-factory.ts: 统一的测试数据工厂 - 支持创建用户、产品、新闻、联系表单数据 - 支持批量创建测试数据 - 支持覆盖默认属性 - 提供便捷函数 - test-data-cleaner.ts: 测试数据清理工具 - 自动清理mock函数 - 清理localStorage/sessionStorage - 清理定时器和事件监听器 - 提供withCleanup装饰器 测试覆盖: - test-data-factory.test.ts: 22个测试用例 - test-data-cleaner.test.ts: 9个测试用例 优化效果: - 减少测试代码重复 - 提高测试可维护性 - 标准化测试数据管理
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<T>(fn: () => T): T {
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
TestDataCleaner.cleanup();
|
||||
}
|
||||
}
|
||||
@@ -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('测试联系人');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>): 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>): User {
|
||||
return this.createUser({
|
||||
role: 'admin',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
static createProduct(overrides?: Partial<Product>): 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>): 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>): 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<T>(
|
||||
factory: () => T,
|
||||
count: number = 3
|
||||
): T[] {
|
||||
return Array.from({ length: count }, () => factory());
|
||||
}
|
||||
|
||||
static createUsers(count: number = 3, overrides?: Partial<User>): User[] {
|
||||
return this.createMany(() => this.createUser(overrides), count);
|
||||
}
|
||||
|
||||
static createProducts(count: number = 3, overrides?: Partial<Product>): Product[] {
|
||||
return this.createMany(() => this.createProduct(overrides), count);
|
||||
}
|
||||
|
||||
static createNewsList(count: number = 3, overrides?: Partial<News>): News[] {
|
||||
return this.createMany(() => this.createNews(overrides), count);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTestUser(overrides?: Partial<User>): User {
|
||||
return TestDataFactory.createUser(overrides);
|
||||
}
|
||||
|
||||
export function createTestProduct(overrides?: Partial<Product>): Product {
|
||||
return TestDataFactory.createProduct(overrides);
|
||||
}
|
||||
|
||||
export function createTestNews(overrides?: Partial<News>): News {
|
||||
return TestDataFactory.createNews(overrides);
|
||||
}
|
||||
|
||||
export function createTestContactFormData(overrides?: Partial<ContactFormData>): ContactFormData {
|
||||
return TestDataFactory.createContactFormData(overrides);
|
||||
}
|
||||
Reference in New Issue
Block a user