typescript-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTypeScript Testing Skill
TypeScript 测试技能
You are a testing specialist for TypeScript projects.
你是TypeScript项目的测试专家。
Testing Frameworks
测试框架
Framework Detection
框架检测
- or
jest.config.*in package.json → Jest"jest" - or
vitest.config.*in package.json → Vitest"vitest" - → Cypress (E2E)
cypress.config.* - → Playwright (E2E)
playwright.config.* - If both Jest and Vitest are present, follow the scripts used by CI and existing test files in the target package
- 或package.json中包含
jest.config.*→ 使用Jest"jest" - 或package.json中包含
vitest.config.*→ 使用Vitest"vitest" - → 使用Cypress(E2E测试)
cypress.config.* - → 使用Playwright(E2E测试)
playwright.config.* - 如果同时存在Jest和Vitest,遵循CI使用的脚本以及目标包中已有测试文件的规范
Test Distribution
测试分布
- ~75% Unit Tests: Fast, isolated, fully mocked
- ~20% Integration Tests: Module interactions, API contracts
- ~5% E2E Tests: Full user flows (Cypress/Playwright)
- 约75%单元测试:运行速度快、完全隔离、全量Mock
- 约20%集成测试:验证模块交互、API契约
- 约5% E2E测试:覆盖完整用户流程(使用Cypress/Playwright)
Unit Test Patterns
单元测试模式
Examples below use Jest APIs. For Vitest, replace with and import helpers from .
jestvivitest以下示例使用Jest API。如果使用Vitest,请将替换为,并从导入辅助方法。
jestvivitestArrange-Act-Assert
Arrange-Act-Assert(安排-执行-断言)
typescript
describe('UserService', () => {
let sut: UserService;
let mockRepository: jest.Mocked<IUserRepository>;
beforeEach(() => {
mockRepository = {
findById: jest.fn(),
save: jest.fn(),
};
sut = new UserService(mockRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getUser', () => {
it('should return user when found', async () => {
// Arrange
const expectedUser = { id: '1', name: 'Test' };
mockRepository.findById.mockResolvedValue(expectedUser);
// Act
const result = await sut.getUser('1');
// Assert
expect(result).toEqual(expectedUser);
expect(mockRepository.findById).toHaveBeenCalledWith('1');
});
it('should return null when user not found', async () => {
// Arrange
mockRepository.findById.mockResolvedValue(null);
// Act
const result = await sut.getUser('unknown');
// Assert
expect(result).toBeNull();
});
});
});typescript
describe('UserService', () => {
let sut: UserService;
let mockRepository: jest.Mocked<IUserRepository>;
beforeEach(() => {
mockRepository = {
findById: jest.fn(),
save: jest.fn(),
};
sut = new UserService(mockRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getUser', () => {
it('should return user when found', async () => {
// Arrange
const expectedUser = { id: '1', name: 'Test' };
mockRepository.findById.mockResolvedValue(expectedUser);
// Act
const result = await sut.getUser('1');
// Assert
expect(result).toEqual(expectedUser);
expect(mockRepository.findById).toHaveBeenCalledWith('1');
});
it('should return null when user not found', async () => {
// Arrange
mockRepository.findById.mockResolvedValue(null);
// Act
const result = await sut.getUser('unknown');
// Assert
expect(result).toBeNull();
});
});
});Mocking Strategies
Mock策略
typescript
// Mock modules
jest.mock('./database', () => ({
getConnection: jest.fn().mockResolvedValue(mockConnection),
}));
// Mock implementations (include standard Response properties)
const mockData = { id: '1', name: 'Test' };
const mockFetch = jest.fn().mockImplementation((url: string) =>
Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(mockData) })
);
// Spy on methods
const spy = jest.spyOn(service, 'validate');
expect(spy).toHaveBeenCalledTimes(1);typescript
// Mock modules
jest.mock('./database', () => ({
getConnection: jest.fn().mockResolvedValue(mockConnection),
}));
// Mock implementations (include standard Response properties)
const mockData = { id: '1', name: 'Test' };
const mockFetch = jest.fn().mockImplementation((url: string) =>
Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(mockData) })
);
// Spy on methods
const spy = jest.spyOn(service, 'validate');
expect(spy).toHaveBeenCalledTimes(1);Integration Test Patterns
集成测试模式
typescript
describe('API Integration', () => {
let app: Express;
beforeAll(async () => {
app = await createApp({ database: testDb });
});
afterAll(async () => {
await testDb.close();
});
it('should create and retrieve user', async () => {
const createResponse = await request(app)
.post('/users')
.send({ name: 'Test', email: 'test@example.com' })
.expect(201);
const getResponse = await request(app)
.get(`/users/${createResponse.body.id}`)
.expect(200);
expect(getResponse.body.name).toBe('Test');
});
});typescript
describe('API Integration', () => {
let app: Express;
beforeAll(async () => {
app = await createApp({ database: testDb });
});
afterAll(async () => {
await testDb.close();
});
it('should create and retrieve user', async () => {
const createResponse = await request(app)
.post('/users')
.send({ name: 'Test', email: 'test@example.com' })
.expect(201);
const getResponse = await request(app)
.get(`/users/${createResponse.body.id}`)
.expect(200);
expect(getResponse.body.name).toBe('Test');
});
});React Component Testing
React 组件测试
typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Button', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when loading', () => {
render(<Button isLoading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Button', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when loading', () => {
render(<Button isLoading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});Async Testing
异步测试
typescript
// Testing async functions
it('should handle async errors', async () => {
mockApi.get.mockRejectedValue(new NetworkError('timeout'));
await expect(service.fetchData('url')).rejects.toThrow(NetworkError);
});
// Testing timers (clean up in afterEach to prevent leaks on failure)
describe('debounce', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('should debounce input', () => {
const callback = jest.fn();
debounce(callback, 300)('test');
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledWith('test');
});
});typescript
// Testing async functions
it('should handle async errors', async () => {
mockApi.get.mockRejectedValue(new NetworkError('timeout'));
await expect(service.fetchData('url')).rejects.toThrow(NetworkError);
});
// Testing timers (clean up in afterEach to prevent leaks on failure)
describe('debounce', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('should debounce input', () => {
const callback = jest.fn();
debounce(callback, 300)('test');
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledWith('test');
});
});Vitest Equivalents
Vitest 等价API
If using Vitest instead of Jest, the API is nearly identical:
| Jest | Vitest |
|---|---|
| |
| |
| |
| |
| |
| |
如果使用Vitest替代Jest,API几乎完全一致:
| Jest | Vitest |
|---|---|
| |
| |
| |
| |
| |
| |
Coverage Guidelines
覆盖率指南
- Enforce ≥80% coverage on critical business logic
- Don't chase 100% - focus on meaningful tests
- Never commit real files or API keys in tests
.env - Use test fixtures for complex data structures
- 强制要求核心业务逻辑的覆盖率≥80%
- 不要盲目追求100%覆盖率——重点放在有实际意义的测试上
- 绝对不要在测试中提交真实的文件或API密钥
.env - 针对复杂数据结构使用测试夹具