资讯详情

在 Scala 中用 Mockito 完成 function 中有 new api 操作的单元测试:TaoToken 配置与验证骨架

📅 2026/9/26 9:14:26 | 华诺云谱 👁 阅读
在 Scala 中用 Mockito 完成 function 中有 new api 操作的单元测试:TaoToken 配置与验证骨架
1. 被测函数里直接 new 出 API 客户端测试为什么这么难写在 Scala 项目里写单元测试最容易被卡住的一类场景就是被测函数内部直接new了一个外部依赖。比如下面这种写法很多同学在真实项目里都写过def getLatestData(host: String, port: Int, currentDate: String): Map[String, String] { val api new OtherTeamAPI(host, port) val latestData api.getData(currentDate) // 下面是一堆业务逻辑做过滤、聚合、拼装 latestData }这段代码在本地跑没问题但一旦写单元测试就麻烦了。OtherTeamAPI底层会发起 HTTP 调用测试环境不一定能连通就算能连通返回的数据也不可控。你没法让getData稳定返回一个固定值也就没法断言业务逻辑是否正确。问题的本质是依赖在函数体内部被硬编码创建了。测试代码没有机会把这个依赖替换成 mock 对象。Mockito 能 mock 一个类但它没法拦截new这个动作本身。所以思路不是「让 Mockito 去 mock new」而是把创建依赖的时机从函数内部挪到函数外部让测试可以把 mock 传进去。这篇就围绕这个场景给出可复制的build.sbt依赖、MockitoSugar 与 ArgumentMatchers 用法、mock 注入与 verify 断言骨架以及一段最小可运行测试用例和运行验证命令。适合正在写 Scala 单元测试、被new卡住的同学。2. 前置准备TaoToken 配置与依赖骨架在动手改代码之前先把测试依赖和模型调用环境准备好。这里说的环境是指你在写测试、跑验证、或者用 AI 辅助生成测试骨架时需要一个稳定的模型调用入口。TaoToken 提供统一的 API 接入官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。如果你只是想让模型帮你把一段new依赖的代码改写成可注入的形式可以直接用模型对话入口https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。把被测函数贴进去让它给出「依赖注入 Mockito 测试」的改写版本比手写快很多。长期写 Scala 测试、需要反复生成和迭代测试用例的同学可以看 Coding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它更适合把模型调用嵌进日常编码流程而不是每次单独开对话。真正要跑通下面的测试你需要在 TaoToken 控制台创建 API Keyhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面拿到密钥https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。接入文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求格式说明。注意TaoToken 是合规的 API 接入服务用于模型调用。测试代码里的OtherTeamAPI是你自己项目里的业务依赖两者不要混淆。2.1 build.sbt 依赖片段Scala 项目用 sbt 管理依赖测试相关的库主要是 ScalaTest 和 Mockito。下面这段可以直接复制到build.sbtThisBuild / scalaVersion : 2.13.12 lazy val root (project in file(.)) .settings( name : scala-mockito-demo, libraryDependencies Seq( // 测试框架 org.scalatest %% scalatest % 3.2.17 % Test, // Mockito 核心 org.mockito % mockito-core % 4.11.0 % Test, // ScalaTest 与 Mockito 的桥接提供 MockitoSugar org.scalatestplus %% mockito-4-11 % 3.2.17.0 % Test ) )这里有个容易踩的坑org.scalatest.mockito.MockitoSugar在 ScalaTest 3.2 之后已经废弃正确做法是用org.scalatestplus.mockito.MockitoSugar。如果你照着老教程写import org.scalatest.mockito.MockitoSugar编译会报找不到符号。上面依赖里的mockito-4-11就是新版桥接包版本号要和 Mockito 主版本对应。2.2 被测代码的改写把 new 挪出去回到最开始的问题。要让测试能注入 mock最直接的办法是把依赖作为参数传进来。有两种常见写法。第一种是显式参数class DataService { def getLatestData(api: OtherTeamAPI, currentDate: String): Map[String, String] { val latestData api.getData(currentDate) // 业务逻辑 latestData } }第二种是隐式参数调用方不传时用默认实现测试时传 mockclass DataService { def getLatestData(currentDate: String)(implicit api: OtherTeamAPI): Map[String, String] { val latestData api.getData(currentDate) // 业务逻辑 latestData } }隐式参数的好处是生产代码调用时不用改测试时通过implicit val注入 mock。但要注意隐式参数如果作用域里有多个同类型实例会编译歧义。测试里通常只放一个 mock问题不大。3. 可复制配置MockitoSugar、ArgumentMatchers 与打桩依赖和被测代码准备好后进入测试类编写。下面是一个完整的测试骨架包含 mock 创建、打桩、调用、断言和 verify。3.1 测试类结构与 mock 创建import org.scalatest.funsuite.AnyFunSuite import org.scalatestplus.mockito.MockitoSugar import org.mockito.Mockito.{when, verify, times} import org.mockito.ArgumentMatchers.{anyString, eq eqTo} class DataServiceSpec extends AnyFunSuite with MockitoSugar { test(getLatestData 应返回经过业务处理的数据) { // 1. 创建 mock 对象 val mockApi mock[OtherTeamAPI] // 2. 打桩指定 mock 方法在特定入参下的返回值 val currentDate 2024-06-01 val rawData Map(k1 - v1, k2 - v2) when(mockApi.getData(currentDate)).thenReturn(rawData) // 3. 注入 mock 并调用被测方法 val service new DataService val result service.getLatestData(currentDate)(mockApi) // 4. 断言结果 assert(result rawData) // 5. verify 确认交互发生 verify(mockApi, times(1)).getData(currentDate) } }这段代码里有几个关键点。mock[OtherTeamAPI]由 MockitoSugar 提供它会创建一个该类型的代理对象所有方法默认返回 null 或默认值。when(...).thenReturn(...)是打桩告诉 mock「当调用getData(2024-06-01)时返回 rawData」。注意thenReturn里的类型必须和getData的返回类型一致否则编译不过。3.2 ArgumentMatchers 的用法与陷阱实际测试里入参往往不是固定值这时要用 ArgumentMatchersimport org.mockito.ArgumentMatchers.{anyString, any, eq eqTo} // 匹配任意字符串 when(mockApi.getData(anyString())).thenReturn(rawData) // 匹配任意对象 when(mockApi.getData(any[String])).thenReturn(rawData) // 精确匹配某个值其余用 matcher when(mockApi.getData(eqTo(2024-06-01))).thenReturn(rawData)这里有个高频报错一旦某个参数用了 matcher所有参数都必须用 matcher。比如getData(anyString(), fixed)会抛InvalidUseOfMatchersException正确写法是getData(anyString(), eqTo(fixed))。这个坑我踩过不止一次报错信息不太直观排查起来费时间。另外anyString()在 Mockito 4.x 里不匹配 null如果需要匹配 null用any()或nullable(classOf[String])。3.3 参数对照表场景写法说明固定入参打桩when(mock.getData(d1)).thenReturn(r)精确匹配任意入参打桩when(mock.getData(anyString())).thenReturn(r)匹配任意字符串混合匹配when(mock.getData(anyString(), eqTo(1)))全部参数都要 matcher验证调用次数verify(mock, times(2)).getData(anyString())确认交互验证从未调用verify(mock, never()).getData(anyString())反向断言抛异常打桩when(mock.getData(anyString())).thenThrow(new RuntimeException(boom))测异常分支4. 验证请求与成功结果配置写完后跑测试验证。在项目根目录执行sbt testOnly DataServiceSpec如果一切正常输出类似[info] DataServiceSpec: [info] - getLatestData 应返回经过业务处理的数据 [info] Run completed in 1 second, 234 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed.看到All tests passed就说明 mock 注入成功被测函数里的业务逻辑在隔离环境下跑通了。整个过程没有发起真实 HTTP 调用测试稳定且快。如果你想验证异常分支可以加一个测试test(getData 抛异常时应向上传播) { val mockApi mock[OtherTeamAPI] when(mockApi.getData(anyString())).thenThrow(new RuntimeException(api down)) val service new DataService val ex intercept[RuntimeException] { service.getLatestData(2024-06-01)(mockApi) } assert(ex.getMessage api down) }intercept是 ScalaTest 提供的异常捕获断言比 try-catch 更简洁。5. 本篇常见错排查写这类测试时报错集中在几个地方。下面按现象、原因、解决方式列出来。报错一not found: value mock原因没有混入MockitoSugar或者 import 了废弃的org.scalatest.mockito.MockitoSugar。解决确认测试类extends AnyFunSuite with MockitoSugarimport 用org.scalatestplus.mockito.MockitoSugar依赖里加mockito-4-11桥接包。报错二InvalidUseOfMatchersException原因同一个方法调用里部分参数用 matcher部分用原始值。解决所有参数统一用 matcher固定值用eqTo(value)包一层。报错三when(...)打桩不生效返回 null原因打桩时的入参和实际调用时的入参不相等。比如打桩用2024-06-01实际传的是2024-6-1字符串不匹配。解决用anyString()放宽匹配或者打印实际入参确认值。另外如果被测方法内部对入参做了转换再调用 API打桩要用转换后的值。报错四NullPointerException在 mock 方法上原因mock 对象的方法默认返回 null如果被测代码直接对返回值做操作就会 NPE。解决给所有会被调用的方法打桩或者用RETURNS_DEEP_STUBS不推荐容易掩盖问题。更稳妥的是显式打桩每个用到的方法。报错五隐式参数注入失败编译报ambiguous implicit values原因作用域里有多个同类型隐式实例。解决测试里只保留一个implicit val mockApi或者改用显式参数传递。提示如果测试涉及模型调用相关的验证比如用 AI 生成测试用例后要跑通可以先用模型对话确认代码逻辑https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。接入细节看文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。6. 把 mock 注入固化成团队规范new依赖难测试这个问题本质是代码结构问题不是 Mockito 的问题。Mockito 只能 mock 你给它的对象没法拦截new。所以真正的解法是依赖注入把创建依赖的责任从函数内部移到调用方。落地时建议做两件事。第一在团队里约定涉及外部 API、数据库、文件系统的依赖一律通过参数传入不在函数体内new。第二测试类统一用MockitoSugarArgumentMatchers的组合打桩和 verify 成对出现保证交互被验证。对于长期维护的 Scala 项目测试用例会越写越多模型辅助生成骨架能省不少时间。需要把模型调用嵌进编码流程的可以看 Coding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。API Key 在控制台创建https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 密钥管理在 API Keys 页面https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。最后留一个实用技巧写完测试后把verify的调用次数从times(1)改成times(2)跑一遍如果测试失败说明你的断言真的在起作用而不是被 mock 的默认行为蒙混过关。这个反向验证能帮你确认测试的有效性。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑