如何使用Mockito和ScalaTest模拟Scala伴生对象属性
问题根因
Scala 中定义的object单例编译后会生成final修饰的类,原生Mockito默认不支持mock final类;同时最初实现里的val pool : DataSource = getDataSource会在伴生对象类加载初始化阶段直接执行真实的getDataSource逻辑,后续mock操作赶不上这个初始化时机,自然无法命中mock逻辑。
可行解决方案
方案1:开启Mockito final mock支持 + 懒加载改造
如果要保留全局单例的写法,按以下步骤调整:
- 开启Mockito对final类的mock支持:在测试资源目录创建文件
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker,文件内容填写:
mock-maker-inline
- 调整
CoreDataSource的连接池初始化逻辑为懒加载,避免类加载阶段就触发真实连接池创建:
object CoreDataSource { // lazy修饰后,首次访问pool时才会执行初始化,给mock留出切入时机 lazy val pool : DataSource = getDataSource def getDataSource: DataSource = { // 真实的连接池创建逻辑 } }
- 测试代码中使用
mockito-scala提供的withObjectMocked作用域包裹测试逻辑,对单例对象打桩:
@RunWith(classOf[JUnitRunner]) class MySuite extends AnyFunSuite with MockitoSugar with Matchers { test("Test mocking the Datasource") { withObjectMocked[CoreDataSource.type]{ val mockDs = mock[DataSource] val mockConn = mock[Connection] // 配置mock返回逻辑 when(CoreDataSource.getDataSource).thenReturn(mockDs) when(mockDs.getConnection).thenReturn(mockConn) // 执行测试逻辑 val query = new Query() val result = query.getSalutations // 断言与验证 assert(result == "") verify(mockDs).getConnection } } }
之前尝试的
Option版本懒加载逻辑也可以正常使用,注意确保getDataSource不会在mock生效前被触发即可,Scala原生的lazy val是线程安全的懒加载实现,比手动写Option判断更简洁。
方案2:依赖注入改造(生产环境推荐)
从架构层面解耦,不要在业务类中硬依赖全局静态对象,通过构造器传入DataSource依赖,从根源上避免mock静态对象的问题:
// 将DataSource作为构造参数传入,业务代码不关心实例来源 class Query(source: DataSource) { def getSalutations: String = { val conn : Connection = source.getConnection // 业务逻辑 } } // 生产环境代码直接传入全局连接池即可 val productionQuery = new Query(CoreDataSource.pool)
测试时直接传入mock的DataSource实例即可,完全不需要操作静态单例,不会遇到final类mock的问题:
@RunWith(classOf[JUnitRunner]) class MySuite extends AnyFunSuite with MockitoSugar with Matchers { test("Test getSalutations without real datasource") { val mockDs = mock[DataSource] val mockConn = mock[Connection] when(mockDs.getConnection).thenReturn(mockConn) // 直接传入mock依赖 val query = new Query(mockDs) val result = query.getSalutations assert(result == "") verify(mockDs).getConnection } }
该方案不需要依赖Mockito的高级mock特性,测试逻辑稳定无黑魔法,是行业内通用的可测试性最佳实践。
方案3:增加测试覆写入入口
如果不想调整现有依赖逻辑,可以给CoreDataSource增加测试专用的实例设置入口,测试时直接替换连接池为mock实例,不需要mock静态object本身:
object CoreDataSource { private var _pool: DataSource = _ // 仅测试阶段调用,替换连接池实例 def setTestDataSource(ds: DataSource): Unit = { _pool = ds } def pool: DataSource = { if (_pool == null) { _pool = getDataSource } _pool } private def getDataSource: DataSource = { // 真实连接池创建逻辑 } }
测试时直接调用CoreDataSource.setTestDataSource(mockDs)传入提前准备好的mock对象即可,实现简单,不会触发final类mock的报错。
内容的提问来源于stack exchange,提问作者Jackie
相关产品推荐
相关产品推荐

