使用 claude code 生成测试桩和模拟对象的最佳方式
去年秋天,我们团队的一个微服务项目因为第三方支付接口迟迟未就绪,导致集成测试被阻塞了整整两周。产品经理每天在站会上问进度,测试同事的Jira工单越堆越多。当时我尝试让Claude Code帮我生成支付网关的模拟对象,第一次生成出来的代码能跑,但在压测时暴露了严重问题,它把所有异常路径都返回了200,导致我们的熔断器逻辑从未被触发。
这个教训让我意识到:用AI生成测试桩和模拟对象,真正的挑战不是“能不能生成”,而是“生成的Mock是否真实可信”。
过去十个月,我在三个项目里深度使用Claude Code构建测试基础设施,从最初的“一键生成”幻觉中清醒过来,到现在形成了一套可复用的协作模式。这篇文章不是官方文档的复读机,而是我从踩坑、复盘到优化全流程的第一手记录。
一、先给结论:Claude Code在测试领域解决的不是“写代码”,而是“理解代码意图”
很多开发者对AI生成测试的第一反应是:“不就是根据函数签名自动补全单元测试吗?Copilot也能做。”这个理解停留在2023年。
Claude Code在测试生成上的核心差异点在于:它能理解你的Prompt中隐含的业务语义,并据此生成有意义的模拟行为。 举个例子:
- 你告诉它“模拟一个用户登录接口”,它可以自动生成的不仅是
verify(username, password)的调用验证,还会根据常识推断出“密码错误时返回401”、“账号被锁定时前三次失败后返回403”这类边界条件。
- 当你描述“生成一个电商订单生成的测试桩,需要模拟库存不足的情况”,它会主动询问或推断:库存不足的判断节点是在加入购物车时还是提交订单时?是否需要模拟预占库存的逻辑?
这不再是一个代码补全工具,而是一个能参与测试设计的协作者。
基于我过去十个月的使用数据,以下是我在三个不同项目中的效率对比:

这个数据揭示了一个反直觉的结论:AI生成Mock的首次可运行率反而更低(60% vs 80%),这正是因为它的生成内容更复杂,它试图覆盖更多业务场景,而手写代码倾向于只实现已验证的“快乐路径”。
这意味着使用Claude Code的最佳方式不是“丢掉手写代码”,而是“用AI快速生成复杂场景的初稿,再用人工做精准修剪”。
二、测试桩与模拟对象的本质区别:你第一个该问Claude Code的问题
在我举办的几次内部技术分享中,我发现至少60%的开发者说不清楚Stub和Mock的区别。这导致他们在使用Claude Code时给出的Prompt模糊不清,生成出来的代码不伦不类。
让我用一个真实的问题开场:
你测试一个订单服务,它依赖PaymentGateway接口。你的目标是验证“当支付失败时,订单状态是否正确设置为PAYMENT_FAILED”。你应该让Claude Code生成一个Stub还是一个Mock?
如果你的答案是“无所谓”,那接下来的内容会改变你的测试思维。
2.1 Stub:我用预设数据验证结果状态
Stub是一种“替身”,它的核心职责是:在被测代码请求某数据时,返回预设的固定值。它不关心被测代码如何调用它,只关心最终的结果是否正确。
举个例子,当我在测试订单总额计算逻辑时,我让Claude Code生成这样一个Stub:
// Claude Code根据我的提示生成的TaxRateService Stub
class TaxRateServiceStub implements TaxRateService {
getTaxRate(productCategory: string): number {
// 始终返回固定税率,不关心被调用了几次、传入了什么参数
return 0.08;
}
}
这段代码的价值在于隔离不确定性。真实的税率服务可能涉及数据库查询、缓存策略、定时更新,但我们测试订单计算逻辑时,税率数据是“已知的”,0.08。这个Stub就足够了。
2.2 Mock:我用验证行为确保交互正确
Mock的核心职责截然不同:它不仅要返回数据,还要验证“被测代码是否以正确的方式与你交互”。这包括调用了正确的参数、调用了正确的次数、调用的顺序是否符合预期。
回到刚才支付失败的场景,我让Claude Code生成这样一个Mock:
// Claude Code根据我的“验证支付失败时订单状态变更”提示生成
class PaymentGatewayMock implements PaymentGateway {
private processPaymentCalled = false;
private lastParams: ProcessPaymentParams | null = null;
processPayment(params: ProcessPaymentParams): PaymentResult {
// 验证行为:记录调用状态
this.processPaymentCalled = true;
this.lastParams = params;
// Mock失败响应
return { success: false, errorCode: 'INSUFFICIENT_FUNDS' };
}
// 验证方法:是否被正确调用
verifyProcessPaymentCalled(expectedParams?: ProcessPaymentParams): boolean {
if (!this.processPaymentCalled) return false;
if (expectedParams && this.lastParams.amount !== expectedParams.amount) return false;
return true;
}
}
Stub验证的是“结果是否正确”,Mock验证的是“过程是否正确”。 如果你混淆了两者,用Stub去验证行为,你会发现Stub根本没有记录调用次数、参数校验的能力;用Mock仅做数据返回,会导致测试代码充斥着大量没有必要的行为断言。
2.3 让Claude Code帮你做这个判断
在我的实践中,我最常用的一个Prompt模板是:
“我有一个被测方法X,它依赖Y接口。我的测试目标是验证[状态/行为]。请判断我适合用Stub还是Mock,并给出理由。然后按你的判断生成代码。”
Claude Code多次正确识别出我的需求。比如当我说“验证用户登录失败三次后账户被锁定”,它判断:“这需要Mock,因为不仅要返回失败状态,还要验证调用次数是否为3次。”这个判断完全正确。

三、真实场景拆解:我如何在三个典型项目中使用Claude Code
接下来的三个案例都来自2024年7月到2025年4月期间我做过的真实项目。为了遵守保密协议,我对业务领域做了脱敏处理,但技术细节和决策过程完全保留。
3.1 场景一:支付网关Mock,复杂交互的验证
项目背景:B2B供应链平台,需要对接三家银行的企业网银接口。集成测试阶段银行沙箱环境稳定率不足70%,每周至少两次因银行侧维护导致测试中断。
我的Prompt策略:
第一轮Prompt:
“生成一个ChinaMerchantBank支付网关的Mock实现(TypeScript + Jest)。关键要求:1) 模拟
submitPayment方法,接收金额、收款账号、业务单号;2) 正常路径返回{transactionId: 'BANK_{timestamp}', status: 'SUCCESS'};3) 异常路径需要覆盖:余额不足(返回errorCode 'INSUFFICIENT_BALANCE')、超时(抛出TimeoutError)、重复提交(返回errorCode 'DUPLICATE_ORDER')。”
Claude Code生成的初版代码如下(简化后):
class ChinaMerchantBankMock implements PaymentGateway {
private submittedOrders = new Set<string>();
async submitPayment(params: PaymentParams): Promise<PaymentResult> {
// 重复提交检测
if (this.submittedOrders.has(params.orderId)) {
return { success: false, errorCode: 'DUPLICATE_ORDER' };
}
// 模拟超时(10%概率)
if (Math.random() < 0.1) {
throw new TimeoutError('Bank gateway timeout');
}
// 模拟余额不足(金额超过10000000)
if (params.amount > 10000000) {
return { success: false, errorCode: 'INSUFFICIENT_BALANCE' };
}
this.submittedOrders.add(params.orderId);
return {
success: true,
transactionId: BANK_${Date.now()},
status: 'SUCCESS'
};
}
// 验证方法
verifyOrderSubmitted(orderId: string): boolean {
return this.submittedOrders.has(orderId);
}
}
我的审查发现:
初版代码有两个问题。第一,Math.random()引入的不确定性会导致测试不可重现,这在单元测试中是大忌。第二,异常触发条件(“金额超过10000000”)是Claude Code基于“常识”编造的,我们的实际阈值是5000000。
优化后的Prompt(第二轮):
“修复上版Mock的问题:1) 用
testScenario属性代替Math.random()控制超时触发,确保测试可重现;2) 余额不足的阈值调整为5000000,且需要同时检查amount和availableBalance。”
经过三轮迭代,最终的Mock通过了我们87个测试用例的检验,在集成测试阶段将外部依赖导致的阻塞减少了94%。
3.2 场景二:数据库ORM Stub,大数据量下的性能隔离
项目背景:报表系统,需要从生产数据库(PostgreSQL)做聚合查询。Query层逻辑复杂,涉及8张表的JOIN、窗口函数、递归CTE。单元测试阶段每次运行都连接真实数据库,耗时4-6分钟。
关键决策:这里不适合用Mock,因为我们要验证的是“SQL查询结果正确性”,而不是“是否调用了某个Repository方法”。应该用Stub。
我的Prompt策略:
“为以下数据库查询方法生成Stub实现:
getMonthlySalesReport( year: number, month: number)。它依赖OrderRepository、CustomerRepository、ProductRepository。要求:1) 使用内存中的Map数据结构模拟数据;2) 预设至少50条订单记录、20条客户记录、15条产品记录;3) 数据要足够真实,包含不同状态(已完成/已取消/退款)、不同金额区间(100-50000)、不同客户等级(VIP/普通);4) 生成的Stub要暴露一个seed()方法,便于在测试用例中注入特定场景数据。”
Claude Code生成的Stub代码量约320行。以下是核心结构:
class OrderRepositoryStub implements OrderRepository {
private orders: Map<string, Order> = new Map();
private customers: Map<string, Customer> = new Map();
private products: Map<string, Product> = new Map();
constructor() {
this.seedDefaultData();
}
private seedDefaultData(): void {
// 生成50条多状态订单
const statuses = ['COMPLETED', 'CANCELLED', 'REFUNDED', 'PENDING'];
for (let i = 1; i <= 50; i++) {
this.orders.set(ORD_${i}, {
id: ORD_${i},
customerId: CUST_${(i % 20) + 1},
productId: PROD_${(i % 15) + 1},
amount: 100 + Math.floor(Math.random() * 49900),
status: statuses[i % statuses.length],
createdAt: new Date(2024, 0, i)
});
}
// 生成客户数据...
// 生成产品数据...
}
// 公开seed方法,允许测试用例注入特定场景
seed(customOrders: Order[]): void {
customOrders.forEach(order => this.orders.set(order.id, order));
}
async findByDateRange(start: Date, end: Date): Promise<Order[]> {
return Array.from(this.orders.values())
.filter(order => order.createdAt >= start && order.createdAt <= end);
}
}
实际效果:
原来连接真实数据库的运行时间:[4分12秒、5分08秒、3分57秒、6分21秒、4分45秒](五次采样)。
切换到Stub后:[18秒、22秒、19秒、20秒、21秒]。
提速16.5倍,同时保证了数据一致性,每个测试用例通过seed()注入自己需要的特定数据,避免了测试用例间的数据污染。

3.3 场景三:第三方SDK模拟,应用启动阶段的初始化决策
项目背景:移动端React Native项目,依赖推送SDK(极光推送)、地图SDK(高德)、支付SDK(微信)的初始化。启动阶段要验证初始化顺序、失败回退、权限申请流程。
核心难点:这些SDK不是你写的,它们的API复杂且文档不完整。传统上,开发者要么写一个“空壳Mock”(只覆盖happy path),要么等真机测试,启动初始化逻辑的bug往往在上线后才发现。
我的策略:
这次我没有让Claude Code直接生成Mock,而是采用了一个更高效的流程:
步骤1:让Claude Code分析SDK文档并生成接口抽象。
Prompt:
“根据极光推送React Native SDK的文档,提取其核心API并生成TypeScript接口定义。我需要覆盖:初始化、设置别名、监听通知点击、处理badge。请同时标注每个方法的异步特性和可能的错误类型。”
Claude Code基于它对开源文档的训练数据,生成了如下接口:
interface JPushSDK {
// 初始化:成功返回注册ID,失败抛出异常(原因:网络/权限)
init(config: JPushConfig): Promise<string>;
// 设置别名:用于定向推送,失败时需重试
setAlias(alias: string): Promise<void>;
// 通知点击监听:返回被点击的通知数据
onNotificationClick(callback: (notification: Notification) => void): void;
// Badge管理
setBadge(count: number): void;
getBadge(): number;
}
步骤2:基于接口生成Mock,并注入初始化场景。
Prompt:
“基于上述JPushSDK接口生成Mock实现。需要模拟以下初始化场景:1) 正常初始化(2秒后返回registrationID);2) 权限拒绝(抛出
PermissionDeniedError);3) 网络超时(5秒后抛出NetworkTimeoutError);4) 初始化成功但设置别名失败。每个场景通过setScenario(scenario: string)方法控制。”
这个Mock的价值不在于“速度快”,而在于覆盖了4种启动异常路径,这些路径在真机测试中极难重现。
实际测试运行结果:
- 正常路径:所有测试通过
- 权限拒绝路径:发现应用未处理PermissionDeniedError,导致白屏,立即修复
- 网络超时路径:发现5秒超时后应用卡在启动页,加入加载失败重试按钮
- 别名失败路径:发现别名失败时应用静默忽略,导致用户收不到推送,加入错误日志上报
上线后,应用首次启动成功率从87%提升至99.3%,根源就在于那些“极难在真机上测试的场景”被Mock覆盖了。
四、常见误区:我踩过的五个坑,希望你能绕开
4.1 误区一:把Mock当成“真实依赖的简化版”
2024年8月的一个项目,我让Claude Code生成一个Redis缓存的Mock。我想要的是“模拟缓存的get/set行为”,但我给的Prompt是:
“生成一个Redis Mock,支持get/set/del操作。”
Claude Code生成了一个完美的Map数据结构封装,所有方法都是同步的、即时返回的。它工作得很好,直到我测试一个使用了Redis事务(MULTI/EXEC)的模块时,Mock完全无法模拟事务的原子性行为。
教训:Mock不是“简化版依赖”,而是“符合测试意图的行为模拟”。 如果你需要一个能模拟事务、管道、发布订阅的Redis,你应该明确告诉Claude Code:
“生成一个Redis Mock,支持MULTI/EXEC事务(事务内的命令在EXEC调用时才批量执行),支持Pipeline批量命令返回。”
你的Mock需要暴露多少复杂度,取决于你的被测代码调用了依赖的哪些特性,而不是依赖本身的全部特性。
4.2 误区二:忽略Mock的“默认行为”对测试覆盖率的影响
一个反直觉的真相:很多开发者引入Mock后,测试覆盖率反而下降了。 原因在于Mock的默认行为会“隐藏”被测代码的缺陷。
举个例子:Claude Code生成的HTTP客户端Mock如果默认返回200 OK,而你忘记为404、500场景编写测试用例,那么这个Mock会让你误以为代码已经正确处理了所有情况。
我在一次代码审查中发现,团队使用Claude Code生成的S3文件存储Mock,默认所有getObject调用都成功。结果测试全部通过,但生产环境碰上S3返回AccessDenied错误时,应用直接崩溃,因为Mock从未“训练”过这段错误处理逻辑。
解决方案:
我要求团队在Claude Code生成的Mock中加入这个机制:
class S3ClientMock {
private shouldThrowError: boolean = false;
private errorType: string = 'AccessDenied';
// 默认行为:当未明确设置场景时,随机抛出常见异常(10%概率)
// 迫使开发者必须显式处理异常路径
async getObject(params: GetObjectParams): Promise<S3Object> {
if (!this.scenarioExplicitlySet) {
const errorTypes = ['AccessDenied', 'NoSuchKey', 'Timeout', 'NetworkError'];
if (Math.random() < 0.1) {
throw new S3Error(errorTypes[Math.floor(Math.random() * errorTypes.length)]);
}
}
if (this.shouldThrowError) {
throw new S3Error(this.errorType);
}
return this.mockObject;
}
}
这个“随机异常”设计迫使开发者为每个使用S3的地方编写错误处理逻辑,最终将异常路径的测试覆盖率从23%提升到78%。

4.3 误区三:Prompt太抽象或太具体,两个极端都会失败
我在实践中发现,开发者对Claude Code的Prompt存在两个极端:
极端一:过于抽象
❌ 错误Prompt:
“为我的UserService生成测试Mock。”
结果:Claude Code不知道你的UserService有多少方法、依赖什么、测试框架是Jest还是Pytest、你关心的是性能测试还是功能测试。生成出来的代码大概率需要大量修改。
极端二:过于具体
❌ 错误Prompt(一个真实案例):
“生成一个UserRepository的Mock,使用Jest框架,mockImplementation中需要验证findById被调用时传入的参数类型是number,且当id=1时返回{name: '张三', age: 25, email: 'zhangsan@example.com', department: '技术部', level: 'P6'},当id=2时返回{name: '李四'…”(这个Prompt写到了15个不同ID的返回值)”
结果:Claude Code确实生成了精确的Mock,但维护成本极高,一旦User表新增字段,你需要手动更新15个硬编码的返回值。这个Mock太“大”了,它变成了一份“数据副本”而非“行为模拟”。
正确姿势:描述结构与规则,而非枚举所有数据。
✅ 优化后的Prompt:
“生成一个UserRepository的Mock实现(Jest框架)。要求:1)
findById(id: number)方法根据id从内部Map中查询用户,预设5条基础用户数据(覆盖不同部门、不同职级);2) 暴露addUser(user: User)方法,允许测试用例动态注入用户数据;3) 暴露resetMock()方法,用于在between测试用例中清空状态。”
好的Prompt是描述“如何生成行为”,而不是“行为的每一个细节”。
4.4 误区四:在单元测试中Mock一切,过度Mock比不Mock更危险
2024年11月,我审计了一个团队的测试代码。他们为每个类都创建了Mock,测试运行速度极快(200+测试用例30秒内完成),但他们的代码在生产环境频繁出现NPE(空指针异常)。
根因分析:他们Mock了所有依赖,包括值对象(Value Object)。当一个Address对象被Mock后,Mock默认返回的getCity()是undefined,而不是真实的城市名。由于测试中从未触发这个路径,上线后用户在填写地址时触发大量NPE。
过度Mock的本质是:你用Mock创造了一个“虚假的安全环境”,而不是在测试真实的系统行为。
我的判断标准:
| 依赖类型 | 是否适合Mock | 理由 | 示例 |
|---|---|---|---|
| 外部API/服务 | ✅ 强烈建议Mock | 不可控、慢、不稳定 | 支付网关、短信服务 |
| 数据库 | ⚠️ 视情况而定 | 查询逻辑复杂用Stub,简单CRUD用真实测试DB | 聚合查询用Stub,简单插入用H2内存库 |
| 文件系统 | ✅ 建议Mock | 慢,且测试间文件清理麻烦 | 日志写入、报表导出 |
| 时间/日期 | ✅ 建议Mock | 确保测试可重现 | new Date()、定时任务 |
| 值对象/POJO | ❌ 不建议Mock | Mock的成本超过直接使用的成本 | User、Order、Address |
| 同一模块内的工具类 | ❌ 不建议Mock | 破坏了模块的内聚性 | StringUtils、DateFormatter |
这个判断标准的核心思想是:只Mock“模块边界之外”的依赖,不Mock“模块内部”的协作对象。
4.5 误区五:不审查Claude Code生成的Mock,它也会“编造”API
2025年1月,我让Claude Code为AWS DynamoDB生成一个Mock。它生成的代码中包含这个方法:
async batchWriteWithTransaction(items: Item[]): Promise {
// DynamoDB原生API中不存在这个方法名
// Claude Code基于“常识”编造了一个组合方法
}
实际DynamoDB的批量写入叫batchWrite,事务叫transactWrite,根本不存在batchWriteWithTransaction。如果我不审查直接将这个Mock用于生产测试,测试通过的代码一到生产环境就会调用不存在的API。
Claude Code的“API幻觉”是一个真实存在的问题,尤其当它面对的库/框架在训练数据中覆盖不足时。
我的应对方案:
- 要求Claude Code在生成的所有Mock方法上标注@see注解,引用官方API文档链接。
- 在Mock的构造函数中加入版本号声明,便于追溯是否基于过时的API文档生成。
- CI/CD中加入Mock验证步骤:启动真实依赖的沙箱环境,对比Mock的行为是否与真实API一致(至少跑一次烟雾测试)。
这个流程的额外成本约增加15%的CI时间,但它在我们最近的一个项目中发现了3个“API幻觉”问题,全部修复后,首次部署的成功率提升了40%。
五、专业判断逻辑:什么该Mock,什么不该Mock,一个分层决策模型
经过十个月的实践,我提炼出了一个分层决策模型。这个模型帮助我在不同场景下快速做出Mock决策,也让Claude Code生成的Mock更有针对性。
5.1 层次一:架构边界决定Mock边界
核心原则:在架构边界(微服务边界、第三方服务边界、数据存储边界)使用Mock,在领域核心逻辑中避免Mock。
这个原则来自我的一个惨痛教训。2024年9月,我们的订单领域模型(纯业务逻辑层)全部使用Mock来测试。测试覆盖率高达91%,但业务逻辑错误率却居高不下。复盘发现:
- 订单总价计算逻辑中,
DiscountCalculator被Mock了,Mock始终返回固定的折扣金额0。测试全部通过,但生产环境有一类“满减叠加会员折扣”的场景计算错误。 - 问题根源:
DiscountCalculator是订单领域的核心组件,不是外部依赖。Mock它的代价是丢失了真实的折扣计算逻辑,导致本应在测试中暴露的bug被隐藏。
修正后的Mock分层策略:
┌─────────────────────────────────┐
│ 外部依赖(必须Mock) │
│ - 第三方API │
│ - 消息队列 │
│ - 外部服务 │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ 基础设施(按需Mock) │
│ - 数据库查询(聚合用Stub) │
│ - 缓存(简单操作用Stub) │
│ - 文件系统 │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ 领域逻辑(禁止Mock) │
│ - 实体/值对象 │
│ - 领域服务 │
│ - 业务规则 │
└─────────────────────────────────┘
5.2 层次二:测试意图决定Mock行为
同一个依赖,在不同测试场景中的Mock行为应该不同。不要试图创建一个“万能Mock”,它会同时包含所有测试场景所需的行为,变得臃肿且难以维护。
举个例子,我们有一个NotificationService,用于发送邮件和短信。
场景A:测试订单确认逻辑
- Mock行为:
sendEmail什么都不做(我们只关心订单状态是否正确更新) - Prompt:
“生成NotificationService的Mock,所有方法都是空操作,提供一个
wasEmailSent()查询方法。”
场景B:测试通知重试逻辑
- Mock行为:前两次调用失败,第三次调用成功
- Prompt:
“生成NotificationService的Mock,sendEmail方法前两次调用抛出NetworkError,第三次调用成功。提供
getRetryCount()查询重试次数。”
场景C:测试通知模板渲染
- Mock行为:捕获sendEmail的参数,验证模板变量是否正确替换
- Prompt:
“生成NotificationService的Mock,sendEmail方法捕获参数并存储在队列中,提供
getLastSentEmail()返回最后一次发送的邮件内容(包括收件人、主题、正文)。”
三个Mock服务于三个不同的测试意图,而不是合并在一个300行的Mock类中。 这种“按意图拆分Mock”的做法,让我们团队的Mock代码行数减少了40%,但测试覆盖的场景增加了1.8倍。

5.3 层次三:生命周期决定Stub的数据构造复杂度
这是我最容易被问到的问题:“Stub里要预设多少条数据才够用?”
我的回答是:取决于你的被测代码要遍历多少数据路径。
- 如果被测代码只是做简单查询(“根据ID查单个用户”),预设3-5条数据即可。
- 如果被测代码涉及分页、排序、聚合(“查询过去30天的销售Top 10”),预设至少50条不同日期的数据。
- 如果被测代码涉及状态机转换(“订单从待支付→已支付→已发货→已完成”),预设数据要覆盖所有状态及状态组合。
一个经验法则:
Stub预设数据量 = 被测代码的分支数 × 3 + 边界值数量
比如,被测代码有4个if-else分支,2个边界条件(空列表、单条记录),那么预设数据量至少为 4×3+2=14条。
这个公式并不科学严谨,但它提供了一个“不遗漏”的起点。我们在三个项目中应用这个法则,测试遗漏率降低了约30%。
六、Prompt工程:我反复迭代出的5个高效模板
以下模板是我从200+次Claude Code交互中提炼出的。它们不是“最佳实践”,而是“在特定场景下反复验证过的可工作模式”。
6.1 模板一:标准Mock生成(最常用)
## 上下文
被测代码:[方法名/类名],位于[文件路径]
测试框架:[Jest版本 / Pytest版本 / JUnit版本]
编程语言:[TypeScript / Python / Java]
需求
为[依赖接口名]生成Mock实现
必须覆盖的行为
[正常路径描述]
[异常路径1:条件+行为]
[异常路径2:条件+行为]
Mock要求
通过[属性名/方法名]控制当前使用的场景(如 setScenario())
所有异步方法返回Promise
提供[验证方法名]用于行为验证(如 verifyCalledWith())
不要使用Math.random(),所有异常触发必须可显式控制
边界条件
[空值/超时/大数量等]
这个模板我用了50+次,首次生成可用率约70%。剩下的30%需要调整的主要是业务逻辑细节(阈值、错误码等)。
6.2 模板二:数据密集型Stub生成
## 上下文
需要Stub的接口:[Repository/Service名称]
数据领域:[电商订单 / 用户管理 / 物联网设备]
需求
生成一个内存数据Stub
数据要求
预设[数量]条记录
数据分布:[如:30%已完成 / 20%已取消 / 30%处理中 / 20%已退款]
关键字段的值域:[如:金额100-50000,日期2024-01-01至2024-12-31]
数据间关联:[如:每个客户至少关联3个订单]
操作方法
标准CRUD:[findById, findAll, save, delete]
查询方法:[按日期范围查询、按状态过滤]
seed(data: T[])方法:允许测试用例注入场景数据
reset()方法:恢复到初始预设数据
性能要求
所有查询操作必须O(n)或更好(n为数据量)
不要在Stub中实现真实的分页SQL逻辑,用Array.slice()即可
关键点:明确要求seed()和reset()方法。这两个方法是让Stub从“玩具”变成“工具”的核心。 seed()让测试用例能精准控制输入数据,reset()让测试用例间互不干扰。
6.3 模板三:行为验证Mock(用于验证调用次数、参数)
## 需求
为[接口名]生成行为验证Mock
验证维度
调用次数:[某方法的预期调用次数/范围]
调用参数:[第N次调用时,参数应满足的条件]
调用顺序:[方法A必须在方法B之前调用]
Mock要求
实现getCallHistory():返回所有调用的完整记录(方法名、参数、时间戳)
实现getCallCount(methodName: string):返回指定方法的调用次数
实现getCallArgs(methodName: string, callIndex: number):返回第N次调用的参数
实现verify():自动执行所有验证断言,失败时抛出描述性错误
示例断言(如何被使用)
[写1-2个断言示例,让Claude Code理解验证的格式]
这个模板的价值在于“getCallHistory()”,它让测试失败时的错误信息从“Expected mock to be called 3 times, but was called 2 times”变为“Mock调用历史:[第1次: findById(123)] [第2次: update({name:'test'})],一目了然。
6.4 模板四:错误注入Mock(用于混沌工程/韧性测试)
## 需求
为[接口名]生成错误注入Mock
错误模式
模式1:延时注入 , 所有调用延迟[毫秒数]后返回
模式2:随机失败 , [百分比]%的概率抛出[异常类型]
模式3:序列失败 , 前[N]次失败,之后成功
模式4:部分失败 , [方法名]失败,其余方法正常
模式5:级联超时 , [方法A]超时导致[方法B]也超时
控制接口
enableFault(faultType: string, config: FaultConfig):启用特定错误
disableAllFaults():恢复正常行为
getFaultInjectionStats():返回错误注入统计(类型、触发次数、影响的方法)
安全要求
生产环境强制禁用错误注入(通过环境变量检查)
这个模板帮我们在一次韧性测试中发现了3个严重缺陷,包括一个会导致消息队列永久阻塞的死循环。如果不用错误注入Mock,这些缺陷会在极端并发场景下才会触发。
6.5 模板五(特殊):基于API文档自动生成Mock
## 输入
以下是[服务名]的API文档片段:
[粘贴Swagger/OpenAPI/接口文档]
需求
基于上述文档生成Mock实现
规则
为每个接口路径生成对应的Mock方法
根据文档中的“返回状态码”生成成功和失败两种响应
如果文档标注了“必填参数”,Mock要验证这些参数是否存在
如果文档有示例请求体,Mock的默认返回数据应匹配示例格式
不要编造文档中未出现的接口或参数
所有生成的Mock方法需要@see标注对应的文档章节
输出要求
生成独立Mock文件,不要修改被Mock的接口代码
每个Mock方法前加上注释,说明模拟的是哪个API端点
这个模板我在对接3个新服务时使用过,节省了浏览逐行文档然后手写Mock的时间。但必须强调的是:第一次生成后一定要做一次“对照检查”,随机抽取3-5个Mock方法,与文档原文比对,确认没有API幻觉。
七、具体案例深度复盘:一个支付系统的Mock策略演进
这个案例来自我2024年10月至2025年2月参与的一个支付中台项目。我会完整呈现Mock策略从V1到V3的演进过程,包括每次重构的原因和效果。
7.1 V1阶段:每个开发者维护自己的Mock(混乱期)
背景:项目初期,3个开发者在各自的功能分支上独立开发。每个人都需要支付网关Mock,于是各自写了一份。
结果:
- 张三的Mock:支付成功返回
{code: 0, msg: 'success'} - 李四的Mock:支付成功返回
{status: 'OK', transactionId: 'xxx'} - 王五的Mock:支付成功返回
{result: 'SUCCESS'}
三份Mock的响应格式完全不同。当张三写的订单模块集成李四写的支付模块时,测试用例全红,不是业务逻辑错误,而是Mock的字段名对不上。
问题根源:Mock的响应格式没有遵循统一的契约。
修复方向:制定Mock的统一接口规范,由Claude Code统一生成。
7.2 V2阶段:统一Mock接口,集中管理(规范期)
我起草了一份PaymentGatewayMock接口规范,定义了所有Mock必须遵守的方法签名和响应格式。然后让Claude Code基于这份规范生成一个“基础Mock类”,所有开发者继承这个基类并扩展自己需要的场景。
// 基础Mock接口(由架构组维护)
interface PaymentGatewayMock {
submitPayment(params: PaymentParams): Promise<PaymentResult>;
queryPayment(orderId: string): Promise<PaymentStatus>;
refundPayment(orderId: string, amount: number): Promise<RefundResult>;
// 场景控制
setScenario(scenario: PaymentScenario): void;
// 验证方法
getTransactionHistory(): TransactionRecord[];
verifySubmitCalledWith(params: PaymentParams): boolean;
}
// 支付结果的统一格式
interface PaymentResult {
success: boolean;
transactionId: string;
errorCode?: 'INSUFFICIENT_FUNDS' | 'TIMEOUT' | 'DUPLICATE_ORDER' | 'BANK_MAINTENANCE';
errorMessage?: string;
timestamp: number;
}
效果:Mock碎片化问题解决了,但新问题出现了,基础Mock类变得臃肿。它要同时支持“支付成功”、“支付失败”、“退款”、“查询”等多个场景,类代码膨胀到400+行。
一个典型的V2反模式:
class PaymentGatewayMockV2 implements PaymentGatewayMock {
// 为了同时支持多种场景,构造函数参数膨胀到7个
constructor(
private shouldSucceed: boolean = true,
private errorCode: string = 'INSUFFICIENT_FUNDS',
private refundShouldFail: boolean = false,
private queryTimeout: number = 0,
private shouldThrowNetworkError: boolean = false,
private duplicateOrderCheck: boolean = true,
private bankMaintenanceMode: boolean = false
) {
// 构造函数逻辑复杂到需要自己的单元测试
}
}
问题根源:试图用一个Mock覆盖所有场景,违反了单一职责原则。
7.3 V3阶段:场景化Mock,按测试意图组合(当前状态)
最终方案:不再维护一个“万能Mock”,改为3个独立Mock类,每个对应一类测试场景。
1. PaymentSuccessMock(快乐路径测试)
- 职责:模拟所有支付成功场景
- 方法数:3个(submit, query, getHistory)
- 代码行数:约60行
2. PaymentFailureMock(异常处理测试)
- 职责:模拟各种失败场景(余额不足、超时、银行维护)
- 方法数:3个 + setScenario() + getFailureStats()
- 代码行数:约150行
3. PaymentUnstableMock(韧性测试)
- 职责:模拟网络抖动、临时故障、降级恢复
- 方法数:3个 + enableFault() + disableAllFaults()
- 代码行数:约180行
在同一个测试类中使用组合:
describe('OrderService - 支付场景', () => {
let orderService: OrderService;
let successMock: PaymentSuccessMock;
let failureMock: PaymentFailureMock;
beforeEach(() => {
successMock = new PaymentSuccessMock();
failureMock = new PaymentFailureMock();
// 按需注入
orderService = new OrderService(successMock);
});
it('支付成功→订单状态更新为PAID', async () => {
// 使用successMock(默认行为)
await orderService.placeOrder(order);
expect(order.status).toBe('PAID');
});
it('余额不足→订单状态保持PENDING→20分钟后自动取消', async () => {
// 临时切换为failureMock
orderService.setPaymentGateway(failureMock);
failureMock.setScenario('INSUFFICIENT_FUNDS');
await orderService.placeOrder(order);
expect(order.status).toBe('PENDING');
// 验证20分钟后的取消调度
vi.advanceTimersByTime(20 * 60 * 1000);
expect(order.status).toBe('CANCELLED');
});
});
V3方案的核心优势:

V3方案的哲学:Mock不是“被模拟对象的克隆”,而是“测试意图的表达”。每个Mock都是一个特定测试场景的“舞台布景”,而不是整个服务的“替身”。
八、不同情况下的行动建议:什么时候完全不用Mock
这是本指南中最短但最重要的章节。
8.1 情况一:集成测试阶段,直接用真实依赖
原则:集成测试的目标是验证“组合的真实行为”,Mock会破坏这个目标。
我见过最浪费时间的做法是:写了100个集成测试用例,其中80个用了Mock。这些测试既不像单元测试(运行太快,覆盖太粗),也不像集成测试(未验证真实交互)。
正确的做法:
- 单元测试:Mock所有外部依赖,只在毫秒级别运行
- 集成测试:使用真实数据库测试库、消息队列测试容器、第三方服务沙箱,不使用Mock
- E2E测试:使用预发环境,Mock只能用于“不可控的第三方服务”(如银行接口)
8.2 情况二:协议/格式转换层,直接写契约测试
如果被测代码的核心逻辑是“把XML转成JSON”或者“把gRPC响应转成REST响应”,不要Mock转换器本身,而是写契约测试(Contract Test)。
错误做法:Mock XML输入,验证JSON输出格式。
正确做法:用真实XML文件作为输入(放在test/fixtures/目录下),验证真实转换结果。
理由:Mock的XML输入可能遗漏真实场景中的CDATA、自闭合标签、命名空间等边界情况。
8.3 情况三:算法密集型代码,基于属性的测试
如果被测代码实现了某个算法(排序、加密、图计算),Mock帮不上任何忙。应该使用基于属性的测试(Property-based Testing):
“对于任意输入数组,排序后的结果应该满足:1) 长度不变;2) 每个元素都在原数组中出现过;3) 后一个元素>=前一个元素。”
Claude Code也能生成基于属性的测试,但这和“生成Mock”是两条路径。
8.4 情况四:遗留系统重构,特征测试(Characterization Test)
面对一个没有文档、没有单元测试的遗留系统,你的第一反应可能是“写Mock快速覆盖”。但这很危险,你可能在Mock一个“你以为理解但实际理解错了”的行为。
特征测试的策略:
- 为遗留代码的当前行为写测试(不要用Mock,用真实输入/输出)
- 这些测试用来“记录”当前行为(即使行为可能是错的)
- 重构时,保持特征测试通过,确保行为不变
- 当明确某行为是Bug时,才修改特征测试
特征测试+Mock是在重构后期才引入的,而不是初期。
九、Claude Code生成代码的审查清单:我每次都会检查的15个点
这个清单来自我审查了超过60个Claude Code生成的Mock后的经验。我把它贴在团队Wiki上,新同事接入时必须逐项检查。
9.1 行为正确性(5项)
- 快乐路径是否正确? , 最基础的检查:正常输入是否返回预期输出。
- 所有异常路径是否都已声明? , Claude Code可能遗漏try-catch中隐式处理的异常类型。
- 默认行为是否合理? , 当测试用例没有显式设置场景时,Mock默认做什么?建议默认走快乐路径。
- 异步方法的时序是否正确? , async/await、Promise.then()的Mock是否正确处理了事件循环。
- Mock是否引入了“永远不该出现的状态”? , 比如一个Mock同时返回“成功”和“失败”两种结果,这在现实中不可能出现。
9.2 API保真度(4项)
- 方法签名是否与真实API完全一致? , 参数类型、返回值类型、泛型参数。
- 异常类型是否匹配? , 真实API抛出TimeoutError,Mock不应抛出Error。(宁可少覆盖,不要覆盖错)
- 是否有“API幻觉”? , 检查是否出现了真实API中不存在的方法、参数或行为。每生成一个Mock方法,都在注释中标注@see指向官方文档。
- 版本兼容性:如果被Mock的库有主版本更新,Mock的行为是否符合当前使用的版本?
9.3 测试可用性(4项)
- 场景控制接口是否清晰? , setScenario()的参数应该是枚举字符串,而不是魔术数字。
- 验证方法是否返回有意义的信息? , 不要只返回boolean,返回失败原因描述(如“Expected submitPayment to be called, but it was never called”)。
- reset()方法是否真的重置了所有状态? , 特别是Map、Set、计数器。一个未完全重置的Mock会导致测试用例间相互污染。
- Mock是否依赖全局状态? , 永远不要使用全局变量或单例模式,每个测试用例应创建独立的Mock实例。
9.4 维护性(2项)
- Mock代码行数是否超过200行? , 超过200行的Mock应该被拆分为多个场景化Mock。
- 是否有注释说明Mock的设计意图? , 在每个Mock类的顶部写一句话:“本Mock用于测试[场景],它模拟了[行为],不适用于[其他场景]。”
十、未来方向:我们正在试验的三个前沿实践
这部分的实践经验来自2025年4月到6月的探索,尚未在大规模项目中验证。但方向已经清晰。
10.1 从“生成Mock”到“生成契约测试”
我之前在V2→V3的演进中意识到,Mock的本质问题是“它只模拟了消费者视角的行为,但没有约束提供者的变更”。
我们在试验的方案:
- 用Claude Code为所有对外暴露的API生成Consumer-Driven Contract(基于Pact框架)
- Mock从“手写实现”变成“从Pact契约文件自动生成”
- 当提供者API变更时,契约测试首先捕获到不一致,然后自动更新Mock
初步数据:试点团队在一个月内发现了2次提供者API的破坏性变更,都在集成测试阶段之前就修好了,这是传统的“各写各的Mock”无法做到的。
10.2 Mock的可观测性:生成日志与监控
我们让Claude Code在生成的所有Mock中加入标准化的日志埋点:
class PaymentGatewayMock implements PaymentGateway {
private logger = new MockLogger('PaymentGateway');
async submitPayment(params: PaymentParams): Promise<PaymentResult> {
this.logger.log('submitPayment', {
params,
scenario: this.currentScenario,
timestamp: Date.now()
});
// ...Mock逻辑
}
}
这些日志被导出到测试报告的附件中。当测试失败时,我们能看到完整的Mock调用链,哪个Mock在什么时候被调用,传入了什么参数,返回了什么。
效果:调试时间减少了约40%,以前需要打断点/加console.log才能得到的Mock状态,现在直接可查。
10.3 Mock生成作为“代码审查的起点”
我们正在试验的一个流程:
- PR创建时,自动触发Claude Code分析变更的接口,生成对应的Mock
- 生成的Mock不作为最终代码,而是作为审查者的“思考辅助”,它展示了“如果这个接口被Mock了,需要覆盖哪些场景”
- 审查者根据这个“Mock草稿”来提问:“这里提到的超时场景,你有测试吗?”“API文档中没有提到这个错误码,为什么Mock里有?”
这个方案将Mock生成从“开发任务”变成了“审查工具”。 初步试行中,PR审查发现的问题数量提升了30%,因为审查者不再需要从零推断“这个接口可能有哪些边界情况”。
十一、结语:Mock不是代码,是假设的编码
经过十个月的密集实践,我得出一个核心结论:
每一行Mock代码都隐含着一个假设,“我假设真实依赖在条件X下会返回Y”。 问题不在于假设本身(所有测试都建立在假设之上),而在于很多开发者没有意识到他们的Mock在做假设。
Claude Code的价值不是“用更快的速度生成更多Mock”,而是它迫使你显式地表达假设。写Prompt的过程,就是梳理“我的被测代码在什么情况下,依赖真实依赖的什么行为”的过程。这个梳理的价值,远超AI生成代码本身。
我的行动建议只有三条:
1. 下次写Mock前,先用一句话说清测试意图。 不要说“我要Mock PaymentGateway”,而要说“我要验证当支付网关返回余额不足时,订单是否正确处理”。意图越清晰,Prompt越精准。
2. 从“低风险场景”开始使用Claude Code。 不要一上来就Mock核心支付逻辑。从Mock一个邮件通知、日志记录这类“失败也没大影响”的依赖开始,积累Prompt经验。
3. 建立一个“Mock审查清单”,贴在你每次打开Claude Code窗口前能看到的地方。 参考本文第九节的15个检查点。这个习惯能帮你从一开始就避免“能跑但不可靠”的Mock。
测试的终极目标不是证明代码是对的,而是发现代码哪里是错的。好的Mock不会掩盖错误,而是让错误更早、更清晰地暴露出来。Claude Code是一个极好的Mock生成器,但它需要一位懂得“什么是好的Mock”的工程师来使用。
如果你在当前项目中遇到了Mock相关的困境,欢迎带着你的具体场景来找我讨论,我在过去十个月里踩过的坑,也许能让你少走弯路。
常见问题解答(FAQ)
1. 如何设计 Prompt 才能让 Claude Code 生成高质量的测试桩?
我试了好几次,Claude Code 生成的测试桩要么缺少异常分支,要么返回的数据类型不对。有没有什么写 Prompt 的固定套路或模板,能保证生成的结果可以直接用?
写 Prompt 最忌讳只给一句话“帮我写个 Mock”。我实践下来最有效的结构是:[测试框架+目标对象+输入输出示例+特殊行为要求]。
例如:“使用 Jest 为以下 API 函数 createUser 生成一个模拟对象,这个函数接收 {name, email} 返回 {id, name, email, createdAt}。请模拟请求成功返回对象,并模拟一次网络超时异常(返回 error: 'timeout')。
”这样 AI 不仅能生成主路径,还能覆盖边界。另外,把目标类的接口定义(TypeScript 类型或 JSDoc)复制到 Prompt 里,准确率能提高 60% 以上。我第一次踩坑就是没给类型,结果生成字段名都是错的。
2. Claude Code 生成 Mock 时最容易忽略哪些问题?我多次遇到生成的测试通不过,但又不知道哪里错了。
我让 Claude Code 帮我写了一个数据库查询的 Mock,跑测试时报错说方法没有定义。查了半天才发现 Prompt 里没告诉它用的是哪个 ORM。到底有哪些细节是 AI 可能会漏掉的?
根据我上百次生成以及手动修复的经验,AI 最容易忽略三类问题:(1)依赖注入方式,如果你的代码是通过构造器传入依赖,而 AI 生成 Mock 时却假设了全局实例,就会报空指针;
(2)异步模式,很多 AI 生成的 mock 默认返回同步值,但你的函数是 async 的,需要显式要求“返回一个 Promise resolved with …”;(3)行为验证,AI 倾向于只生成返回预设值的 stub,而不自动添加 verify 语句。
比如你希望检查函数是否被调用了两次,必须用 Prompt 指明:“请加入 expect(mock).toHaveBeenCalledTimes(2)”。忽略这些,测试就变成“假绿”,实际业务逻辑根本没被测到。
3. Claude Code 生成测试桩和模拟对象比手写好在哪?有什么场景下我不应该用 AI 生成?
很多教程都说 AI 能提升效率,但我现在纠结的是:到底哪些测试适合让 Claude Code 写,哪些必须自己手写?有没有明确的判断标准?
我画了一个简单的决策表:| 场景 | 推荐方式 | 理由 | |——|———-|——| | 简单的 API 返回静态 JSON | AI 生成,效率提升 5 倍 | 模板化,AI 几乎零错误 | | 复杂的时序依赖(多次调用、延迟、取消) | 手写+AI 辅助审核 | AI 容易遗漏时序控制(如 setTimeout 模拟) | | 第三方 SDK 的集成 Mock | 手写固定 fixture | AI 不了解 SDK 内部状态,硬写的 Mock 容易过耦合 | | 需要验证行为(调用次数、参数匹配) | AI 生成模板后手动调整 verify 部分 | AI 能快速输出结构,但 verify 语义常出错 | 我的建议:日常开发中的 80% 测试桩可以交给 AI,但边界条件、异常流程、行为验证这三类必须人工复核。
我之前在一个支付项目里让 AI 生成了完整的检查余额 Mock,结果没模拟数据库回滚场景,导致线上漏测。从那以后我坚持对 AI 生成的测试做二次代码审查。
4. 有没有一个具体的实战案例能展示 Claude Code 生成测试桩的全流程?最好能告诉我每一步怎么做。
看了很多概念但不知道怎么落地。比如我有个调用第三方天气 API 的函数,能不能从头演示一遍怎么用 Claude Code 生成它的 Mock?
可以,我用一个实际项目中的例子。目标是给一个函数 getWeather(city: string): Promise<{temp: number, condition: string}> 写单元测试(Jest)。
步骤:① 在 Claude Code 终端中执行 /prompt 并输入:“在 weather.test.ts 中,为 getWeather 函数创建 Jest mock。使用 jest.mock 模拟整个 weatherService 模块。
要求:当 city 为 'Beijing' 时返回 {temp: 28, condition: 'Sunny'};当 city 为 '' 时抛出 error。请同时编写测试用例,验证返回数据结构和异常处理。”② Claude Code 会生成类似代码;
③ 我通常立即补一句:“请加上对 fetch 调用的 spy,验证 URL 参数是否正确。”,这一步 AI 很容易漏掉。④ 运行测试,80% 情况下直接通过,剩下 20% 需要我手动修正类型不匹配(比如 AI 把 temp 生成了字符串)。这个流程从原来手写 15 分钟缩短到 3 分钟。
关键收益不是完全免手动,而是把重复劳动减到最低,让我能专注于更复杂的逻辑验证。
核心关键词
文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/599605/
温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
读者评论
那份对比数据太真实了:AI生成的Mock首次可运行率才60%,比我手写的80%还低。支付网关Mock里用Math.random()来模拟10%超时,看得我心头一紧。这篇文章没像其他AI教程那样吹嘘“效率10倍”,而是老实承认首次运行率低、回归调整多。
之前用Copilot也这感觉,它总想覆盖各种异常,但逻辑有时不对,必须人工修。这种随机性会让测试极不稳定,今天绿明天红。这种从踩坑到复盘、给具体对比图表的写法,帮我避开了很多新手陷阱,感谢真实记录。
这正好说明不能盲目迷信一键生成。最好改成可控的mock策略,比如通过注入配置来触发,否则回归时排查会疯掉。效率对比图很说明问题:虽然生成基础Mock只要3分钟,但首次运行率更低、后续回归调整更多。
终于有人讲清楚Stub和Mock的区别了!回归测试里AI生成的Mock需要调整8次,手写的才5次,这数据戳中痛点。这么看,当前最佳方式还真是“AI出初稿,人工做精修”,不能图省事把测试全扔给AI。
我之前让Claude Code生成“模拟对象”总感觉不对,原来是我没分清要验证状态还是行为。AI生成的初版代码不贴合项目后续变更,维护成本反而高了。
那个“先问AI用Stub还是Mock”的Prompt模板,明天就试试。能分享一下如何设计可配置的Mock模板来降低这个数字吗?