ScalaTest 与 munit 是两大主流:前者风格齐全适合大团队立规范,后者极简对齐函数式习惯。本节用同一个订单案例过两套写法,再上属性测试与异步测试两件重武器。
业务规则:金额为负的订单必须被拒绝,正数订单打八五折。
ScalaTest 的 funsuite 风格:
class OrderSpec extends AnyFunSuite: test("负金额订单被拒绝") { assert(Order.create(-1).isLeft) } test("正金额订单打八五折") { assert(Order.create(100).map(_.payable) == Right(85)) }
munit 写法几乎只剩骨架:
class OrderSuite extends munit.FunSuite: test("负金额订单被拒绝") { assertEquals(Order.create(-1).isLeft, true) }
选型口味:团队从 Java/JUnit 来、需要标签分组与丰富断言 DSL,选 ScalaTest;个人与函数式团队追求快反馈,munit 的失败信息更直白。两者都在 8.1 的 sbt 会话里用 test 驱动,可共存于同一项目(迁移期常见)。
例子测试验证"你想到的"输入,属性测试验证"所有"输入满足的不变量:
forAll { (a: Int, b: Int) => // 交换律是不变量,随机一百万组也成立 assertEquals(add(a, b), add(b, a)) } forAll { (xs: List[Int]) => // 排序的不变量:长度不变、结果有序 assertEquals(xs.sorted.length, xs.length) assert(xs.sorted == xs.sorted.sorted) }

它擅长抓到的正是手写用例想不到的:空列表、Int.MinValue、重复元素。为纯函数写性质、为边界写例子,两层配合。
测试 6.1 的异步代码,框架直接接受 Future 断言(自动等待完成):
test("并发取价返回全部结果") { val fut: Future[List[BigDecimal]] = Future.sequence(symbols.map(fetchPrice)) fut.map(prices => assert(prices.size == symbols.size)) // munit/ScalaTest 会等待该 Future,失败即用例失败 }
注意别在测试里 Await——交给框架等待,超时配置在各自框架里声明。
| 层 | 工具 | 说明 |
|---|---|---|
| 纯函数单测 | 任意框架 + 属性测试 | 3.x 章的组合子最受益 |
| 领域分派 | 模式匹配穷举检查 | 编译期已兜底,测试聚焦业务规则 |
| 并发逻辑 | 异步用例 + 虚拟时间 | Akka TestKit 可虚拟时钟 |
| 集成 | Testcontainers | 真实数据库的边界验证 |
第三章那句"类型即文档"在测试上兑现:类型系统已排除的非法状态不需要测试,测试预算全部花在业务规则的真歧义上——这是 Scala 项目测试可以比 Java 项目少而精的结构性原因。
延续第三章的 parse 函数,测试分三层铺开——正常值、边界值、非法值:
class TempParseSpec extends munit.FunSuite: test("正常行解析为读数") { assertEquals(parse("36.6,C"), Some(Reading(36.6, Celsius))) } test("边界:空串与缺字段都得到 None") { assertEquals(parse(""), None) assertEquals(parse("36.6"), None) } test("非法数字不抛异常,静默为 None") { assertEquals(parse("abc,C"), None) } test("两侧空格应被容忍") { assertEquals(parse(" 36.6 , C "), Some(Reading(36.6, Celsius))) }
第三条用例会立刻暴露实现是否用了 toDouble(抛异常)而非 toDoubleOption——这就是测试驱动出的真实缺陷。解读:四条用例覆盖等价类划分、边界、异常路径、宽容性四个维度,比堆一百条随机输入有效。变式:属性测试(ScalaCheck 风格)补第五层,forall 合法字符串 roundtrip(parse(s).map(render)) 应等于原值,一条性质覆盖无穷输入。
单元层 munit 或 ScalaTest,毫秒级;模块层用测试夹具隔离数据库与网络(munit 的 fixtures 语法最省事);端到端层 testcontainers 起真实依赖。三层配比七二一,UI 与集成测试跑得慢,数量失控就是 CI 时长失控。
| 框架 | 风格 | 适合 |
|---|---|---|
| munit | 函数式、轻量、报错友好 | 新项目默认 |
| ScalaTest | 风格多样、生态最老 | 存量代码库 |
| ScalaCheck | 属性驱动 | 算法与解析逻辑 |
munit 的夹具最值得单独学:FunFixture 把"准备-使用-清理"包成一个值,多个测试共享临时目录、数据库连接时既不泄漏又不用继承基类。异步测试用 test(...).future 或 Future 类断言即可,框架自动等待完成——第六章的 Future 管线测试就这么写。
最后一条经验:断言消息里永远带上输入值(assertEquals 的 clue 参数),失败输出里能直接看到"36.6,C 解析成了什么",比只看到一个 None 快十倍定位。