关于 typescript-unit-testing
面向 TypeScript 项目的单元测试技能,按设置、编写、审查、运行、调试和优化等意图选择工作流。提供 NestJS、数据库及消息系统测试参考,并强调隔离依赖、覆盖边界与错误场景。
主要功能
- 01按任务选择测试工作流
- 02提供 NestJS 测试参考
- 03涵盖数据库与消息系统测试
- 04规定 AAA 结构与模拟规则
- 05指导失败测试逐个排查
- 06包含性能与覆盖率要求
适用场景
- 01为新项目配置测试基础设施
- 02为服务和控制器编写单元测试
- 03审查测试质量并补齐边界场景
- 04定位失败测试或处理慢测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bmad-labs/ skills --skill typescript-unit-testing安装后在 Claude Code 中输入/typescript-unit-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEreferences/common/assertions.mddetect-open-handles.mdexamples.mdknowledge.mdperformance-optimization.mdrules.mdreferences/kafka/kafka.mdreferences/mocking/deep-mocked.mdfactories.mdjest-native.mdreferences/nestjs/controllers.mdguards.mdinterceptors.mdpipes-filters.mdservices.mdreferences/redis/redis.mdreferences/repository/mongodb.mdpostgres.mdworkflows/debugging/workflow.mdworkflows/debugging/steps/step-01-init.mdstep-01b-continue.mdstep-02-classify-failure.mdstep-03-analyze.mdstep-04-implement-fix.mdstep-05-verify-fix.mdstep-06-document-resolution.mdworkflows/debugging/templates/output-template.mdworkflows/optimizing/steps/step-01-init.mdstep-01b-continue.md详细介绍
单元测试技能
单元测试通过模拟所有外部依赖,隔离验证单个函数、方法和类。
工作流
如需按步骤指导执行单元测试任务,请使用相应的工作流:
| 工作流 | 用途 | 使用时机 |
|---|---|---|
| Setup | 初始化测试基础设施 | 新项目或缺少测试配置 |
| Writing | 编写新的单元测试 | 为组件创建测试 |
| Reviewing | 审查现有测试 | 代码审查、质量审计 |
| Running | 执行测试 | 运行测试、分析结果 |
| Debugging | 修复失败的测试 | 测试失败,需要诊断 |
| Optimizing | 提升测试性能 | 测试缓慢、需要改善可维护性 |
工作流选择指南
IMPORTANT:开始任何测试任务之前,先识别用户意图并加载相应的工作流。
检测用户意图 → 选择工作流
| 用户表达的需求 | 要加载的工作流 | 文件 |
|---|---|---|
| "Set up tests", "configure Jest", "add testing to project", "install test dependencies" | Setup | workflows/setup/workflow.md |
| "Write tests", "add tests", "create tests", "test this service/controller" | Writing | workflows/writing/workflow.md |
| "Review tests", "check test quality", "audit tests", "are these tests good?" | Reviewing | workflows/reviewing/workflow.md |
| "Run tests", "execute tests", "check if tests pass", "show test results" | Running | workflows/running/workflow.md |
| "Fix tests", "debug tests", "tests are failing", "why is this test broken?" | Debugging | workflows/debugging/workflow.md |
| "Speed up tests", "optimize tests", "tests are slow", "fix open handles" | Optimizing | workflows/optimizing/workflow.md |
工作流执行协议
- ALWAYS load the workflow file first - 采取行动前,先阅读完整的工作流
- Follow each step in order - 按顺序完成各项检查点
- Load knowledge files as directed - 每个工作流都会指定要阅读哪些
references/文件 - Verify compliance after completion - 重新阅读相关参考文件,确保质量
知识库结构
references/
├── common/ # Core testing fundamentals
│ ├── knowledge.md # Testing philosophy and test pyramid
│ ├── rules.md # Mandatory testing rules (AAA, naming, coverage)
│ ├── assertions.md # Assertion patterns and matchers
│ ├── examples.md # Comprehensive examples by category
│ ├── detect-open-handles.md # Open handle detection and cleanup
│ └── performance-optimization.md # Jest runtime optimization
│
├── nestjs/ # NestJS component testing
│ ├── services.md # Service/usecase testing patterns
│ ├── controllers.md # Controller testing patterns
│ ├── guards.md # Guard testing patterns
│ ├── interceptors.md # Interceptor testing patterns
│ └── pipes-filters.md # Pipe and filter testing
│
├── mocking/ # Mock patterns and strategies
│ ├── deep-mocked.md # @golevelup/ts-jest patterns
│ ├── jest-native.md # Jest.fn, spyOn, mock patterns
│ └── factories.md # Test data factory patterns
│
├── repository/ # Repository testing
│ ├── mongodb.md # mongodb-memory-server patterns
│ └── postgres.md # pg-mem patterns
│
├── kafka/ # NestJS Kafka microservices testing
│ └── kafka.md # ClientKafka, @MessagePattern, @EventPattern handlers
│
└── redis/ # Redis cache testing
└── redis.md # Cache operations, health checks, graceful degradation
按任务分类的快速参考
编写单元测试
- MANDATORY:阅读
references/common/rules.md- AAA 模式、命名、覆盖率 - 阅读
references/common/assertions.md- 断言最佳实践 - 阅读组件专用文件:
- Services:
references/nestjs/services.md - Controllers:
references/nestjs/controllers.md - Guards:
references/nestjs/guards.md - Interceptors:
references/nestjs/interceptors.md - Pipes/Filters:
references/nestjs/pipes-filters.md
- Services:
设置模拟
- 阅读
references/mocking/deep-mocked.md- DeepMocked 模式 - 阅读
references/mocking/jest-native.md- 原生 Jest 模式 - 阅读
references/mocking/factories.md- 测试数据工厂
测试仓库
- MongoDB:
references/repository/mongodb.md - PostgreSQL:
references/repository/postgres.md
测试 Kafka(NestJS 微服务)
- 阅读
references/kafka/kafka.md- ClientKafka 模拟、@MessagePattern/@EventPattern 处理程序、emit/send 测试
测试 Redis
- 阅读
references/redis/redis.md- 缓存操作、健康检查、优雅降级
示例
- 阅读
references/common/examples.md,了解全面的模式示例
优化测试性能
- 阅读
references/common/performance-optimization.md- Worker 配置、缓存、CI 优化 - 阅读
references/common/detect-open-handles.md- 修复阻止进程正常退出的未关闭句柄
调试未关闭句柄
- 阅读
references/common/detect-open-handles.md- 检测命令、常见句柄类型、清理模式
核心原则
0. 上下文效率(输出到临时文件)
始终将单元测试输出重定向到临时文件,而不是控制台。测试输出可能很冗长,会占用大量 Agent 上下文。
重要:在文件名中使用唯一的会话 ID,以避免多个 Agent 并行运行时发生冲突。
# Initialize session (once at start of testing session)
export UT_SESSION=$(date +%s)-$$
# Standard pattern - redirect output to temp file (NO console output)
npm test > /tmp/ut-${UT_SESSION}-output.log 2>&1
# Read summary only (last 50 lines)
tail -50 /tmp/ut-${UT_SESSION}-output.log
# Get failure details
grep -B 2 -A 15 "FAIL\|✕" /tmp/ut-${UT_SESSION}-output.log
# Cleanup when done
rm -f /tmp/ut-${UT_SESSION}-*.log /tmp/ut-${UT_SESSION}-*.md
临时文件(每个 Agent 使用唯一的 ${UT_SESSION}):
/tmp/ut-${UT_SESSION}-output.log- 完整的测试输出/tmp/ut-${UT_SESSION}-failures.md- 逐个修复问题的跟踪文件/tmp/ut-${UT_SESSION}-debug.log- 调试运行/tmp/ut-${UT_SESSION}-verify.log- 验证运行/tmp/ut-${UT_SESSION}-coverage.log- 覆盖率输出
1. AAA 模式(必需)
所有单元测试都必须遵循 Arrange-Act-Assert(准备-执行-断言):
it('should return user when found', async () => {
// Arrange
const userId = 'user-123';
mockRepository.findById.mockResolvedValue({
id: userId,
email: '[email protected]',
name: 'Test User',
});
// Act
const result = await target.getUser(userId);
// Assert
expect(result).toEqual({
id: userId,
email: '[email protected]',
name: 'Test User',
});
expect(mockRepository.findById).toHaveBeenCalledWith(userId);
});
2. 使用 target 表示 SUT
始终将被测系统命名为 target:
let target: UserService;
let mockRepository: DeepMocked<UserRepository>;
3. DeepMocked 模式
使用 @golevelup/ts-jest 创建类型安全的模拟对象:
import { createMock, DeepMocked } from '@golevelup/ts-jest';
let mockService: DeepMocked<UserService>;
beforeEach(() => {
mockService = createMock<UserService>();
});
4. 明确断言
断言确切的值,而不只是断言其存在:
// WRONG
expect(result).toBeDefined();
expect(result.id).toBeDefined();
// CORRECT
expect(result).toEqual({
id: 'user-123',
email: '[email protected]',
name: 'Test User',
});
5. 模拟所有依赖项
模拟外部服务,单元测试中绝不使用真实数据库:
// Unit Test: Mock repository
{ provide: UserRepository, useValue: mockRepository }
// Repository Test: Use in-memory database
const mongoServer = await createMongoMemoryServer();
标准测试模板
import { Test, TestingModule } from '@nestjs/testing';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { MockLoggerService } from 'src/shared/logger/services/mock-logger.service';
describe('UserService', () => {
let target: UserService;
let mockRepository: DeepMocked<UserRepository>;
beforeEach(async () => {
// Arrange: Create mocks
mockRepository = createMock<UserRepository>();
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{ provide: UserRepository, useValue: mockRepository },
],
})
.setLogger(new MockLoggerService())
.compile();
target = module.get<UserService>(UserService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getUser', () => {
it('should return user when found', async () => {
// Arrange
mockRepository.findById.mockResolvedValue({
id: 'user-123',
email: '[email protected]',
});
// Act
const result = await target.getUser('user-123');
// Assert
expect(result).toEqual({ id: 'user-123', email: '[email protected]' });
});
it('should throw NotFoundException when user not found', async () => {
// Arrange
mockRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(target.getUser('invalid')).rejects.toThrow(NotFoundException);
});
});
});
测试覆盖率要求
| 类别 | 优先级 | 说明 |
|---|---|---|
| 正常路径 | 必须 | 输入有效并产生预期输出 |
| 边界情况 | 必须 | 空数组、null 值、边界值 |
| 错误情况 | 必须 | 未找到、验证失败 |
| 异常行为 | 必须 | 正确的类型、错误代码和消息 |
| 业务规则 | 必须 | 领域逻辑、计算 |
| 输入验证 | 必须 | 无效输入、类型不匹配 |
覆盖率目标: 新代码达到 80% 以上
失败处理流程
关键:一次修复一个测试。修复过程中绝不要反复运行完整测试套件。
单元测试失败时:
- 初始化会话(开始时执行一次):
export UT_SESSION=$(date +%s)-$$ - 创建跟踪文件:
/tmp/ut-${UT_SESSION}-failures.md,记录所有失败的测试 - 选择一个失败的测试 - 只处理这一个测试
- 仅运行该测试(绝不运行完整测试套件):
npm test -- -t "test name" > /tmp/ut-${UT_SESSION}-debug.log 2>&1 tail -50 /tmp/ut-${UT_SESSION}-debug.log - 修复问题 - 分析错误并进行针对性修复
- 验证修复 - 将同一测试运行 3-5 次:
for i in {1..5}; do npm test -- -t "test name" > /tmp/ut-${UT_SESSION}-run$i.log 2>&1 && echo "Run $i: PASS" || echo "Run $i: FAIL"; done - 在跟踪文件中将其标记为 FIXED
- 处理下一个失败的测试 - 重复步骤 3-7
- 只有在所有单个测试都通过后,才运行一次完整测试套件
- 清理:
rm -f /tmp/ut-${UT_SESSION}-*.log /tmp/ut-${UT_SESSION}-*.md
原因:运行完整测试套件会浪费时间和上下文。每个失败的测试都会让输出变得冗长,使调试更加困难。
命名约定
测试文件
- 模式:
*.spec.ts - 位置:与源文件放在同一目录
测试结构
describe('ClassName', () => {
describe('methodName', () => {
it('should [expected behavior] when [condition]', () => {});
});
});
变量名
| 变量 | 约定 |
|---|---|
| SUT | target |
| 模拟对象 | 使用 mock 前缀(mockRepository、mockService) |
| 模拟类型 | DeepMocked<T> |
不应编写单元测试的内容
不要为以下内容创建单元测试:
- 接口 - 仅包含类型定义,没有运行时行为
- 枚举 - 静态值映射,没有可测试的逻辑
- 常量 - 静态值,没有行为
- 类型别名 - 仅包含类型定义
- 简单 DTO - 不含逻辑的数据结构
只测试包含可执行逻辑的文件(带有方法的类、包含行为的函数)。
应避免的反模式
| 不要这样做 | 原因 | 改为这样做 |
|---|---|---|
| 只断言存在性 | 无法发现错误的值 | 断言具体值 |
| 使用条件断言 | 结果不确定 | 分开编写测试用例 |
| 测试私有方法 | 与实现细节耦合 | 通过公共接口测试 |
| 在测试之间共享状态 | 会导致测试不稳定 | 在 beforeEach 中重新初始化 |
| 在服务中模拟存储库 | 测试的是实现细节 | 模拟接口 |
| 跳过模拟验证 | 无法验证行为 | 验证模拟调用 |
| 测试接口、枚举或常量 | 没有可测试的行为 | 跳过这些文件 |
检查清单
设置:
- 对被测系统使用
target - 所有模拟对象都使用
mock前缀 - 使用
DeepMocked<T>类型 - 包含
.setLogger(new MockLoggerService()) - 遵循 AAA 模式并添加注释
- 在
afterEach中重置模拟对象
覆盖率:
- 为所有公共方法编写正常路径测试
- 编写边界情况测试(空值、null、边界)
- 编写错误情况测试(未找到、验证失败)
- 验证异常类型和错误代码
- 验证模拟调用(参数 + 次数)
质量:
- 新代码的覆盖率达到 80% 以上
- 不对日志调用进行断言
- 测试之间互不依赖
- 任意字段不同时,测试都会失败
评论
支持 Markdown