Ok itest
Generate executable integration-test specs, skeletons, and PR reports for Spring Boot + Spring AI services. Trigger when the user wants to write integration tests, add wire-level ITs, draft an IT spec, or produce a PR test report.From its SKILL.md
npx -y skills add bob798/ai-skill-kit --skill ok-itestAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
17.1 KB, ~6.2k tokens by cl100k_base, as published. Nobody here has run it
ok-itest — 集成测试生成 skill
单文件 skill。面向 Spring Boot + Spring AI 后端的集成测试(WireMock stub LLM、Testcontainers 真 DB、多租户隔离)。 吸收实战 + TDD skill 的形态约束:anti-pattern 表 + tracer bullet + 短 checklist + 命令式语气。 任何细则若与本文件冲突,以本文件为准。项目内已有规范(CLAUDE.md / 架构文档 / 代码)优先级高于本 skill。
哲学
集成测试 spec 的核心是可验证契约,不是描述清楚。
- 一份合格的 IT spec 必须可以被另一个 agent 照着实现并跑通,无需追加 Q&A。
- 写出来的每一条验收标准都对应一段可执行命令或一个具名
@Testmethod,能输出 PASS/FAIL。 - 报告 = spec 的回答;spec 写多少,报告就必须能验证多少。
反模式(看到这些立刻停手)
| # | 反模式 | 为什么坏 |
|---|---|---|
| AP1 | 一次写完所有场景的 IT,再统一跑(horizontal slice) | 跑挂时定位成本爆炸;早期决策无证据支撑 |
| AP2 | mock 自己控制的内部 service / repository | 测的是 mock 形状,不是真实行为 |
| AP3 | @Nested + @SpringBootTest + @TestPropertySource + @DirtiesContext 组合 | Spring Boot context 缓存语义不可预期,偶发挂 |
| AP4 | WireMock / mock server 用 localhost | VPN/特殊网络下 localhost 可能被劫持到非 127.x |
| AP5 | Interview 问技术细节("用 @MockitoBean 还是 BFPP?") | 这些应由 agent 读代码判断;用户回答没价值 |
| AP6 | spec 字段为了凑齐而填(风险表凑 2 条 / 设计依据预填全部) | 噪音稀释真信号 |
| AP7 | Self-check 用"人工确认 X 已做"代替"运行命令 X 并贴 exit code" | 形式合规,实际没卡住 |
Workflow
四阶段。每阶段都有可观测交付物,不是描述。
阶段 1 — Explore(先查代码,不要问能查到的)
读下表所有目标后再开口问用户。
| 要找的事实 | 在哪儿 |
|---|---|
| 现有 IT base / fakes | */support/wiremock/、*/test/support/ |
| 同类 IT 骨架(最近 1 份) | */integration/*IT.java |
| 既有 spec / plan / 报告 | docs/**/itest/、相邻 spec 目录 |
| 项目领域词汇 | README.md、CLAUDE.md、相邻 prd.md / spec |
| 关键库版本 | pom.xml / build.gradle(Spring AI、WireMock、JaCoCo 等) |
| 多租户上下文事实 | 项目多租户文档 + TenantContext / JwtAuthenticationFilter 等鉴权过滤器(以代码为准) |
交付物:spec 中"复用清单"段已填,每行写明"复用方式(继承 / @Import / 直接调用)"。
阶段 2 — Locate(路径在写一个字符之前定)
路径单一语法(任何 spec 必须严格匹配,否则 self-check 拒绝):
itest-dir = docs/<feature-area>/itest/<NNN-scope>/ # 绑定某 feature
| docs/tech-specs/<capability>/itest/<NNN-scope>/ # 跨 feature 的能力专项
spec = <itest-dir>/spec.md
plan = <itest-dir>/plan.md
reports = <itest-dir>/reports/
report = <itest-dir>/reports/pr-<branch-safe>.md
matrix = <itest-dir>/reports/features.md
scripts = scripts/itest/<scope>-pr-report.sh # repo root,便于 CI
scripts/itest/<scope>-features-matrix.sh
NNN:三位补零,同目录唯一scope:kebab-case,2-5 词branch-safe = branch.replace('/', '-'):避免含/的非法文件名
历史迁移门禁:spec.md 落盘前,检查 itest-dir 父链上是否有旧路径的同主题 spec。命中则:
- 阻断:不允许新 spec 落新路径,先发 issue 或 commit 把旧资产搬过来;或
- 在新 spec frontmatter 加
migrated-from: <旧路径>字段记录引用
交付物:spec 路径符合上述语法,reports/ 子目录已建,旧资产关系明确。
阶段 3 — Interview(只问业务决策,不问技术细节)
必问(每问给推荐答案,一次一个):
- 测试入口层:Controller (MockMvc) / Service / Engine / 其他
- 场景清单:枚举 happy / error / timeout / 边界,每个 = 1 个
@Test - 非目标(这套 IT 不回答什么)
- 回归集(哪些既有 IT 必须不破,给测试类清单或 glob)
- 稳定性门槛(单次 vs 50×;耗时上限;覆盖率最低线)
- 是否允许改生产代码
不要问(agent 应该读代码判断):
- bean 替换技术(BFPP vs
@MockitoBeanvs@ComponentScan(excludeFilters)) - WireMock bind 地址、OpenAI base-url 是否加
/v1 - JaCoCo 版本
- 上下文 key 列表(看鉴权过滤器 + 生产侧 MDC 调用即可)
- Spring context 策略(先按现有同类 IT 推断,仅在结论会引入风险时确认)
- fixture 策略(先按"最小 stub"默认,仅在涉及外部 schema 合规时确认)
交付物:spec 中"Interview 决策"段,6 个必问项各有用户确认的答案。
阶段 4 — Tracer Bullet → Incremental Loop
禁止 horizontal slice(先写完所有 IT 再实现 fakes/stubs/报告链路)。
Tracer Bullet(第 1 个 PR)必须垂直贯穿:
spec → 1 个 happy-path IT → fakes → stubs → base class → 报告脚本 → coverage
跑通后才能进入 incremental loop,每个后续 PR 加 1 个场景。
交付物:
- Tracer-bullet PR 在合入前真跑过一次 PASS,surefire 摘要贴到 plan
<itest-dir>/reports/pr-<branch-safe>.md至少存在 1 份features.md至少汇总 1 个场景
Spec 模板
最小必填 + 条件附加。条件附加段只在触发时出现,未触发就不写。
---
name: <scope-slug>
title: <一句话场景目标>
status: DRAFT | REVIEW | STABLE
version: 1.0
last-updated: YYYY-MM-DD
owner: <user>
parents:
- <相邻 prd.md / tech-spec 路径>
migrated-from: <旧路径> # 仅当有历史资产时
---
# <一句话场景目标>
## §0 测试原则(把项目既有规范反向继承到本 spec)
<把项目 CLAUDE.md / 架构文档 / 多租户文档里与测试相关的不变量,逐条抄成"规则 + 失败处置"。
见文末"把仓库规范反向继承到 spec"。>
## 测试目标
<回答什么 / 不回答什么>
## 复用清单
| 类型 | 资产 | 复用方式 |
| ---- | ---- | -------- |
## Interview 决策
| # | 问题 | 答案 |
| - | ---- | ---- |
| 1 | 测试入口层 | <Controller / Service / Engine / 其他> |
| 2 | 是否允许改生产代码 | <是 / 否> |
| 3 | 稳定性门槛 | <单次 / 50×;耗时 ≤Xs;覆盖率 ≥Y%> |
| 4 | 回归集 | <既有 IT 清单 / glob> |
| 5 | 非目标 | <列表> |
## 场景清单
| ID | 场景 | 触发 | 预期 wire / 状态 | 关联 AC | Test class.method |
| ---- | ---- | ---- | ---------------- | ------- | ----------------- |
| S-01 | … | … | … | AC1 | `FooIT.bar_baz` |
## 验收标准(AC 表必须可执行)
| AC | 描述(假设…当…则…) | 验证方式 |
| -- | ------------------- | -------- |
| AC1 | 假设…,当…,则… | `./mvnw verify -Dtest=FooIT#bar_baz` 且输出含 `<token>` |
| AC2 | 假设…,当…,则… | `FooIT.qux` + `wm.verify(1, postRequestedFor(…))` |
## 外部依赖处理
| 依赖 | 处理方式 | 落地点 | 命中的条件规则 |
| ---- | -------- | ------ | -------------- |
## 上下文准备
| 上下文 | 注入点 | 并发传播 |
| ------ | ------ | -------- |
| MDC.tenant/user/requestId/roles | `@BeforeEach` | … |
| TenantContext | `@BeforeEach` | 仅同线程 |
## 文件级改动清单(只列新增 / 修改,不列"不动")
| 路径 | 动作 | 说明 |
| ---- | ---- | ---- |
## 报告产物
- PR 报告:`./reports/pr-<branch-safe>.md`(脚本:`scripts/itest/<scope>-pr-report.sh`)
- Features matrix:`./reports/features.md`
- **PASS 判定 = 本 spec §验收标准全部满足**
## 范围之外
- <明确不做的事>:<原因>
## ---- 以下段落条件出现 ----
### Coverage(仅 Interview §稳定性门槛要覆盖率时)
- 工具:JaCoCo `<version>`
- 输出:`target/site/jacoco/jacoco.xml`
- 报告渲染:报告脚本读取并展示 §Coverage 段
### 风险表(仅有真实风险时;不要凑数)
| 风险 | 触发条件 | 应对 |
| ---- | -------- | ---- |
### Bean 替换策略(仅需替换被 @ComponentScan 扫到的真实 bean 时)
| 真实 bean | 替换方式 | 验证断言 |
| --------- | -------- | -------- |
### Fixture / Stub / Matcher(仅 fixture 非平凡时)
- JSON 模板位置 / matcher 策略 / stub 工厂
### 设计依据(仅触发"条件规则"表中任意一条时,逐条写一段)
针对**命中的**条件规则,每条解释为什么这么做、考虑过的其他方案、为何放弃。**未触发的不写**。
条件规则(按触发命中,写进 spec 对应段)
每条 = 触发条件 → 强约束 → 在 spec 哪一段体现 → GOOD/BAD 对照。
CR1 — @SpringBootTest IT 全场景
强约束:禁用 @Nested 内嵌测试类,IT 必须是顶层类。
落地段:spec frontmatter status + 文件级改动清单。
CR2 — 使用 WireMock
强约束:bindAddress("127.0.0.1");Spring 属性必须用 http://127.0.0.1:<port>。
落地段:外部依赖处理 + 设计依据。
// GOOD
WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig().bindAddress("127.0.0.1").dynamicPort())
.build();
r.add("spring.ai.openai.base-url", () -> "http://127.0.0.1:" + wm.getPort());
// BAD —— VPN/特殊网络下 localhost 可能解析到非 127.0.0.1
.options(wireMockConfig().dynamicPort()) // 默认 bind 0.0.0.0 / 走 localhost
r.add("spring.ai.openai.base-url", wm::baseUrl); // baseUrl() 永远返回 localhost
CR3 — Spring AI OpenAI (≥ 2.0-M4)
强约束:base-url 末尾不补 /v1(OpenAiApi.Builder 默认带)。
落地段:外部依赖处理。
# GOOD
spring.ai.openai.base-url: http://127.0.0.1:12345
# BAD —— 实际请求会变成 /v1/v1/chat/completions
spring.ai.openai.base-url: http://127.0.0.1:12345/v1
CR4 — 经过 ParallelToolCallingManager / 自定义线程池 / 异步
强约束:MDC 必须显式 put 租户/用户/请求上下文 key,不能只靠 TenantContext ThreadLocal。
落地段:上下文准备。
MDC key 名以项目代码为准(常见坑:业务编码 key 如
tenantCode与历史 UUID key 如tenantId混用)。 落 spec 前先 grep 生产侧MDC.put(...)与鉴权过滤器,确认真实 key 名,不要照搬本 skill 的示例字面量。
// GOOD(在 @BeforeEach;key 名替换为项目真实值)
MDC.put("tenantCode", "it-02-tenant");
MDC.put("userId", "it-02-user");
MDC.put("requestId", "it-02-" + UUID.randomUUID());
TenantContext.set("it-02-tenant");
// BAD —— Parallel/异步 worker 只快照 MDC,不传 ThreadLocal
TenantContext.set("it-02-tenant"); // fan-out worker 拿不到 → dispatcher 报 FORBIDDEN(missing_tenant)
CR5 — 需替换已被 @ComponentScan 扫到的真实 bean
强约束:必须 BeanFactoryPostProcessor.removeBeanDefinition + 测试 Fake。
落地段:Bean 替换策略。
// GOOD
@Bean
static BeanFactoryPostProcessor removeRealTools() {
return bf -> {
if (bf instanceof BeanDefinitionRegistry r) {
removeBeanDefinitionsForType(bf, r, DeviceStatusTool.class);
}
};
}
// BAD —— excludeFilters 对已扫到的 bean 无效;启动直接 "Tool already registered"
@ComponentScan(excludeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = DeviceStatusTool.class))
CR6 — 测试 bean 与生产同名
强约束:测试 bean 必须独立命名 + @Primary,否则同名 override 顺序不稳。
落地段:Bean 替换策略。
// GOOD
@Bean(name = "it_testRetryTemplate")
@Primary
public RetryTemplate retryTemplate() { return RetryTemplate.builder().maxAttempts(1).build(); }
// BAD —— prod 同名 bean 可能反向覆盖测试 bean
@Bean
@Primary
public RetryTemplate retryTemplate() { ... }
CR7 — 报告要 Coverage 且 Java ≥ 21
强约束:JaCoCo ≥ 0.8.14(0.8.12 无法 instrument Java 21+ 字节码)。 落地段:Coverage(仅条件出现时)。
Per-IT Checklist(合入前必过)
行为判定,不是字段是否填齐。每条挂一个具体命令或 grep。
[ ] tracer-bullet PR 真跑过且 PASS(贴 surefire 摘要到 plan)
[ ] 每个 AC 的"验证方式"字段可独立运行,命令带 exit code
[ ] 每个外部依赖都隔离了:grep 跑 IT 时的 stdout/网络日志,没有真实 endpoint / DB 调用
[ ] 若涉及租户:spec 中有"tenant 隔离"断言(非本租户不可见 / 跨租户报错)
[ ] 命中的条件规则在 spec §设计依据 + 对应段(外部依赖 / 上下文 / Bean 替换)双写
[ ] 没命中的条件规则在 spec 中完全不出现
[ ] 报告脚本一行命令产出 `reports/pr-<branch-safe>.md` + `features.md`,CI 可消费
[ ] CI 命令 1 行能跑完整套:`./mvnw verify -Dtest='<...IT>'`
Self-check(Process 承诺覆盖矩阵)
verify 时按矩阵逐行走;任一失败:修正后重跑。每条只 verify Process 阶段承诺过的事,不引入新约束。
| Process 承诺 | 验证方式 |
|---|---|
| 阶段 1:读现有 base/fakes/同类 IT/spec/构建版本 | spec §复用清单 ≥ 1 行;frontmatter 关键库版本写明 |
| 阶段 2:路径符合单一语法 | bash 脚本断言 itest-dir 匹配正则;branch-safe 已替换 / |
| 阶段 2:旧资产已处理 | 若旧 spec 存在 → 新 spec 含 migrated-from 或迁移 issue 链接 |
| 阶段 3:6 个必问项都有用户确认答案 | spec §Interview 决策表无 <占位> |
| 阶段 3:没问技术细节 | spec / plan 中搜不到 "@MockitoBean vs BFPP" / "WireMock bind 地址要选哪个" 等问句 |
| 阶段 4:tracer bullet PR 已 PASS | plan 中贴有 surefire Tests run: X, Failures: 0 摘要 |
| 阶段 4:报告链路通 | <itest-dir>/reports/pr-<branch-safe>.md 与 features.md 真实存在 |
| Per-IT checklist 8 条全过 | 见上节 |
| 条件规则:命中即落地,未命中即不出现 | 对每条 CR:grep spec 关键词触发 → 必须在指定落地段出现;反之必须不在"设计依据"出现 |
关键差异(vs 需求类 PRD skill / mattpocock TDD)
- vs PRD skill:PRD 偏需求表达,字段可松;ok-itest 是可执行契约,AC 必须有验证命令、场景必须有 test class.method。
- vs mattpocock TDD:mattpocock 拆 sibling 文件深挖;本 skill 单文件落地,例子内联,借鉴其 anti-pattern + tracer bullet + 短 checklist + 命令式语气。
把仓库规范反向继承到 spec(每个项目自填)
本 skill 不绑定任何具体仓库。落地到某项目时,先读该项目的 CLAUDE.md / 架构文档 / 多租户文档 / 后端规范,把与测试相关的不变量抽成"规则 + 失败处置",写进每个 spec 顶部的 §0 测试原则。每条带源头链接,self-check 拒绝缺失项。
方法:每行 = 源头文档 §章节 → 抽取的规则 → 在 spec 哪段落地。常见可继承项:
- 多租户不变量(业务表租户列非空、租户编码不可变、跨租户隔离断言)→ Per-IT checklist 第 4 条
- 领域词汇约束(实体命名、术语)→ 测试命名 / 注释规范
- 上下文 key 事实(MDC key 名、
TenantContext持值类型)→ CR4 强约束 - 基础设施约束(Redis 必须走封装 Key/Service、禁裸
RedisTemplate;JWT 走JwtService等)→ §外部依赖处理"处理方式" - 异常分层(业务错 vs 服务错用不同 exception)→ 断言对应 exception 类型
- DB 迁移纪律(DDL 必须走 Flyway/迁移脚本,禁手改 schema)→ §文件级改动清单显式列迁移脚本
示例(某 Spring AI 多租户后端的 §0 形态,供参考,落地时换成你项目的真实文档与规则):
源头 抽取规则 落地方式 CONTEXT.md不可妥协的不变量业务表 tenant_code TEXT NOT NULL;编码不可变;FK 用 codechecklist 强制 tenant 断言;fixture 不改 code 多租户文档 MDC key 是 tenantCode(非tenantId);TenantContext持 String codeCR4 强约束;fake @BeforeEach必填后端规范 Redis 必须 RedisService/CacheService+RedisKey,禁裸RedisTemplate§外部依赖"处理方式"复用封装;测试同禁裸 template 后端规范 业务错 BusinessException(warn)/ 服务错ServiceException(error)断言对应 exception 类型 后端规范 Flyway DDL/DML 走 Flyway,禁手改 schema 涉 schema 变更则 §文件级改动清单列迁移脚本 冲突优先级:项目内规范文档 > 本 skill。代码与文档冲突时,以代码与迁移脚本为事实。
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.