本节摘要:Spock 把测试代码从"验证脚本"提升为"可执行业务规范"。本节讲基于行为驱动开发(BDD)的 given/when/then 结构、where 数据驱动机制、内置 Mock/Stub 与交互断言,以及它如何与 Gradle、CI 无缝集成。读完你能写出业务方也读得懂的测试。
阅读完本节,你应当能够:
多数项目的测试代码是"验证脚本":断言散落、命名随意、逻辑埋在样板里。JUnit 的 @Test + 一堆 assertEquals,读起来既不知道被测组件"在什么场景下应有什么行为",也不知道失败意味着什么。更糟的是,测试与文档脱节——文档说一套,测试测一套。
Spock 的核心转变是:把测试改造成"规范"。测试类叫 Specification(规格类),测试方法叫 Feature Method(特性方法),用 given/when/then 块描述"在什么前置下、做什么动作、应得什么结果"。这种结构强制你按人类叙事逻辑写测试,也让非技术背景的利益相关者能读懂测试意图。测试不再只是验证,而是活文档——代码更新,规范同步更新,文档与代码永不脱节。
class LoginSpec extends Specification { def "用户输入正确密码应登录成功"() { given: '已存在账户' def user = new User(username: 'alice', password: 'secret') def auth = new Authenticator() when: '输入正确密码' def result = auth.login(user, 'secret') then: '登录成功' result.success result.user.username == 'alice' } }
块标签不只是装饰:given 建立前置,when 触发行为,then 断言结果。spock 在编译期通过 AST 转换把这些标签翻译成标准逻辑,开发者享受高级语法的便利,却无运行时损耗。特性方法的方法名是自然语言字符串——"用户输入正确密码应登录成功",这本身就是文档。
测试的一个经典困境是"覆盖所有输入"与"代码不膨胀"的矛盾。Spock 的 where 块把测试逻辑与测试数据分离:
def "计算折扣 #discount"() { expect: discountFor(price, rate) == expected where: price | rate || expected 100 | 0.2 || 80 200 | 0.1 || 180 50 | 0.5 || 25 }
测试引擎自动遍历每一行数据,注入变量,多次执行同一套逻辑。price | rate || expected 表格语法让"输入-输出"一目了然。数据可以来自静态表、范围表达式、甚至外部文件。失败时报告明确指出哪一行数据导致失败——"当 price=50 且 rate=0.5 时测试失败",定位效率极高。

单元测试的核心是隔离依赖。Spock 把 Mock/Stub 内置到框架,无需额外依赖:
def "订单支付应通知邮件服务"() { given: def email = Mock(EmailService) def payment = new PaymentService(email) when: payment.pay(order) then: 1 * email.send(order.id, '支付成功') // 交互断言 0 * email.send(_, '支付失败') }
1 * email.send(...) 断言该方法恰好被调用一次,_ 是任意参数占位符,>> 定义返回值,>> { throw ... } 模拟异常。这种基于交互的验证比状态验证更细腻——你不仅关心订单状态,还关心对象间的协作细节。Mock 引擎基于字节码技术动态生成代理,与测试生命周期深度集成,自动处理依赖注入与清理,避免 Mock 状态污染导致的测试不稳定。
| 实践 | 推荐做法 | 理由 |
|---|---|---|
| 测试命名 | 完整句子描述行为 | 测试即文档 |
| 数据驱动 | where 块 + 边界值 | 覆盖率高、维护低 |
| 依赖隔离 | Mock 优先真实轻实现 | 测试稳定、可复现 |
| 交互断言 | 验证关键协作 | 捕获对象间契约 |
| 报告对接 | JUnit 格式 + JaCoCo | CI 无缝集成 |
💡 关键直觉:把 Spock 测试想成"给系统写使用说明书",但说明书会自动验证自己。业务方读 then 块就能确认"系统该这样表现",而每次构建都在执行这份说明书。
⚠️ 常见坑:Mock 过度——把一切依赖都 Mock 掉,测试与实现细节耦合过紧,一重构就大面积失败。Spock 的设计哲学是"优先使用真实轻量实现,仅在必要时引入 Mock",用 Mock 验证交互、用真实对象验证行为。另一个坑是测试只测"happy path",where 块的真正威力在于覆盖边界与异常分支。
Spock 与 Gradle 的集成几乎零成本:声明 spock-core 依赖,构建工具自动识别执行规格类。测试报告兼容 JUnit 格式,可无缝对接 Jenkins、GitLab CI 等自动化服务器。配合 JaCoCo 覆盖率工具,能精确统计到代码行与分支——结合数据驱动测试,覆盖率分析能清晰展示哪些数据组合尚未覆盖,指导测试补充。这套工具链闭环,是高质量交付的基础设施。
Spock 提供完整扩展点机制:实现扩展接口可拦截测试生命周期各阶段——规格初始化、特性执行前后、结果处理。企业可以开发自定义扩展自动加事务回滚、失败截图、自定义报告。这让 Spock 能适配组织的特定流程需求,而无需改框架源码。
不是替代而是竞争与共存。Spock 基于 JUnit 运行(兼容其运行器),报告格式兼容 JUnit。团队可以 Spock 与 JUnit 共存——需要高度可读的行为规范用 Spock,需要严格的结构化测试用 JUnit。但多数团队引入 Spock 后会把主要测试迁移过去,因为它表达力强得多。
静态数据表、范围表达式、闭包生成的集合、外部文件(CSV、JSON)、数据库查询。只要是 Groovy 能表达的集合都能作为数据源。这让数据驱动测试能应对各种复杂场景。
Stub 提供固定的返回值("返回什么"),Mock 还能验证交互("被调用了几次")。Spock 里 Mock 兼顾两者:mock.method() >> 'value' 定义 Stub 行为,n * mock.method() 做交互断言。实际使用中按需组合。
Spock 的交互断言比表面看到的更强,这里展开三个高级用法。
第一个是条件匹配与通配。_ 匹配任意参数,1 * email.send(_, _) 匹配任意两个参数;_ as String 做类型约束;!null 匹配非空。这些让交互断言能表达"任何订单都要通知"这类宽泛契约,而不必逐条列出参数。
第二个是返回序列与动态响应。>> 'a' >> 'b' >> 'c' 让 Mock 依次返回不同值,模拟调用次数变化的场景;>> { args -> ... } 用闭包动态计算返回值,可以基于入参决定返回。这让 Mock 能模拟复杂的外部行为,而不只是固定值。
第三个是验证顺序。用 then 块里多个交互断言的书写顺序,可以隐含验证调用顺序;需要显式顺序时用 1 * a.foo() 1 * b.bar() 配合。对于时序敏感的业务流程(先鉴权再扣款),顺序验证是必要的保障。
如果团队从 JUnit 迁到 Spock,几个技巧能降低摩擦:JUnit 的 @Before/@After 对应 Spock 的 setup/cleanup 块;JUnit 的 @Test 对应特性方法;断言从 assertEquals 换成 Groovy 的 == 断言(失败信息同样清晰)。迁移不必一刀切——JUnit 测试可以继续跑,Spock 规格类作为补充,逐步迁移。关键是别把 JUnit 的写法硬搬进 Spock,那会浪费它一半的表达力。
某支付团队用 Spock 后,测试代码从"没人愿意读"变成"业务方主动看"。做法是三个转变:测试命名用完整业务句;数据驱动覆盖所有费率组合;交互断言验证"通知、扣款、记账"的完整协作。结果:需求评审时业务方直接引用测试描述讨论行为,回归测试的沟通成本大幅下降。这个案例说明,Spock 的价值不只在于"写测试",更在于它改变了团队描述与讨论系统行为的方式。
Spock 项目要跑得长久,测试的组织方式很关键。三个建议:第一,规格类按"业务能力"而不是"被测类"组织——一个规格类描述一个业务行为的完整场景集合,而不是一个类的所有方法;第二,特性方法名用"主语 + 应 + 行为 + 场景"的句式,保持业务叙事的一致风格;第三,where 块的数据表加注释说明每个数据的业务含义。这样组织出来的测试,本身就是一份可持续演进的业务规范文档。
覆盖率的意义不只是"测了多少行",更是"还有哪些行为没被验证"。Spock 测试配合 JaCoCo:覆盖率报告能指出未覆盖的分支,结合 where 数据表,你能看出哪些数据组合没测到——比如边界值 0、负数、null、超大值。把"覆盖率不足"翻译成"业务行为未验证",是用好覆盖率工具的关键认知。
三个高频反模式值得警惕。一是"测试方法名写成函数名",比如 testCalculatePrice——这浪费了 Spock 的叙事能力,应该写成"订单含税金额计算正确"。二是"一个特性方法塞多个行为",难以定位失败点,应拆分为独立特性。三是"全用 Mock"导致测试与实现耦合——优先真实轻量实现,Mock 只用于真正的外部依赖。这三个纠偏动作,能让测试从"能跑"提升到"有表达力"。
把本节全部要点收进一个规格类,作为可复用的模板:
class OrderServiceSpec extends Specification { def "订单支付成功应扣库存并通知"() { given: '库存服务与邮件服务' def stock = Mock(StockService) def email = Mock(EmailService) def service = new OrderService(stock, email) when: '支付一笔订单' service.pay(order) then: '库存被扣减且通知发出' 1 * stock.deduct(order.sku, order.qty) 1 * email.send(order.customer, '支付成功') where: '多组订单数据' order << [order1, order2, order3] // 动态数据源 } }
这个模板体现了三个核心:BDD 结构(given/when/then 叙事)、交互断言(验证协作)、数据驱动(where 批量覆盖)。把它拆开消化,你就掌握了 Spock 八成的生产用法。剩下的两成,来自你在真实项目里遇到的边界情况——异步测试、多线程场景、与容器测试的集成,这些都可以通过 Spock 的扩展点逐步补齐。测试能力是在项目里长出来的,不是一次读完的。建议把第一个 Spock 规格类写在明天,而不是"等有空"——测试框架的收益,从你写第一个 when/then 就开始累积。别等团队规范定了才动手——一个规格类就能让你直观感受到"测试即文档"的差异。
构建与测试保证了工程质量,下一节看 Grails 如何把 Groovy 的动态性用于 Web 全栈开发。