8.2 测试框架实战


8.2 测试框架实战

ScalaTestmunit 是两大主流:前者风格齐全适合大团队立规范,后者极简对齐函数式习惯。本节用同一个订单案例过两套写法,再上属性测试与异步测试两件重武器。

同一个用例,两种方言

业务规则:金额为负的订单必须被拒绝,正数订单打八五折。

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、重复元素。为纯函数写性质、为边界写例子,两层配合。

异步测试:Future 的断言

测试 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——交给框架等待,超时配置在各自框架里声明。

测试金字塔的 Scala 特有权

工具 说明
纯函数单测 任意框架 + 属性测试 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)) 应等于原值,一条性质覆盖无穷输入。

测试金字塔在 Scala 的落地

单元层 munit 或 ScalaTest,毫秒级;模块层用测试夹具隔离数据库与网络(munit 的 fixtures 语法最省事);端到端层 testcontainers 起真实依赖。三层配比七二一,UI 与集成测试跑得慢,数量失控就是 CI 时长失控。

框架对比与夹具复用

框架 风格 适合
munit 函数式、轻量、报错友好 新项目默认
ScalaTest 风格多样、生态最老 存量代码库
ScalaCheck 属性驱动 算法与解析逻辑

munit 的夹具最值得单独学:FunFixture 把"准备-使用-清理"包成一个值,多个测试共享临时目录、数据库连接时既不泄漏又不用继承基类。异步测试用 test(...).future 或 Future 类断言即可,框架自动等待完成——第六章的 Future 管线测试就这么写。

最后一条经验:断言消息里永远带上输入值(assertEquals 的 clue 参数),失败输出里能直接看到"36.6,C 解析成了什么",比只看到一个 None 快十倍定位。

本节要点回顾

  • ScalaTest 全能、munit 极简,语法不同、sbt 驱动相同,可共存。
  • 属性测试用随机输入验证不变量,专治想不到的边界。
  • 异步用例返回 Future 断言即可,等待交给框架。
  • 类型排除的非法状态免测,预算集中到业务规则。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U