Scala中如何正确为类方法编写单元测试并存根其他方法?优化多方法存根的实现方案
针对你的问题,我有几个更优雅的解决方案,既能隔离待测方法的依赖,又能避免参数过多导致的代码杂乱,同时也能更好地处理Scala Object的测试场景:
1. 最优方案:构造函数注入依赖特质
把需要存根的方法(b()、c()等)提取到一个独立的特质中,然后通过构造函数将这个特质的实例注入到待测类(SUTClass)中。这种方式可以集中管理所有依赖,新增方法时不需要修改待测类的方法参数列表。
重构后的待测代码
// 提取所有依赖方法到特质中 trait SUTDependencies { def b(): Unit def c(): Unit // 新增d()、e()时只需在这里添加方法定义 } // 依赖的真实实现 class RealDependencies extends SUTDependencies { override def b(): Unit = println("Actual implementation of b()") override def c(): Unit = println("Actual implementation of c()") } // 待测类,通过构造函数注入依赖,默认使用真实实现 class SUTClass(deps: SUTDependencies = new RealDependencies) { def a(): Unit = { println("Hello from a()") deps.b() deps.c() } }
测试用例
import org.mockito.MockitoSugar.{doNothing, mock, times, verify} import org.scalatest.FlatSpec class SUTClassTest extends FlatSpec { behavior of "a()" it should "call b() and c() exactly once without executing real implementations" in { // Mock依赖特质 val mockDeps = mock[SUTDependencies] doNothing.when(mockDeps).b() doNothing.when(mockDeps).c() // 传入mock依赖创建SUT实例 val sut = new SUTClass(mockDeps) sut.a() // 验证调用次数 verify(mockDeps, times(1)).b() verify(mockDeps, times(1)).c() } }
这种方案的优势非常明显:新增依赖方法时,只需要在SUTDependencies特质中添加定义,测试代码完全不需要修改,彻底解决了参数膨胀的问题。
2. 快速替代:使用Mockito Spy(部分Mock)
如果不想对原代码做大规模重构,Mockito的Spy功能可以帮你实现部分Mock:创建真实待测对象的Spy实例,然后存根你需要隔离的方法,同时保留其他方法的真实逻辑。
待测代码(无需修改)
class SUTClass { def a(): Unit = { println("Hello from a()") b() c() } def b(): Unit = println("Actual implementation of b()") def c(): Unit = println("Actual implementation of c()") }
测试用例
import org.mockito.MockitoSugar.{doNothing, spy, times, verify} import org.scalatest.FlatSpec class SUTClassSpyTest extends FlatSpec { behavior of "a()" it should "call b() and c() exactly once, skipping real implementations" in { // 创建真实对象的Spy实例 val sutSpy = spy(new SUTClass) // 存根b()和c(),避免执行真实逻辑 doNothing.when(sutSpy).b() doNothing.when(sutSpy).c() // 执行待测方法 sutSpy.a() // 验证调用次数 verify(sutSpy, times(1)).b() verify(sutSpy, times(1)).c() } }
这个方案适合快速验证逻辑,不需要改动原有代码结构,非常灵活。
3. Scala Object的更优测试方式
你提到的mockito-extensions全局配置确实不够理想(可能影响其他测试用例),更干净的做法是将Object中的业务逻辑提取到特质中,让Object继承这个特质,测试时直接Mock该特质即可。
重构后的Object代码
// 提取核心逻辑到特质 trait SUTLogic { def a(): Unit def b(): Unit def c(): Unit } // 原Object继承特质,仅作为单例入口 object SUTObject extends SUTLogic { override def a(): Unit = { println("Hello from a()") b() c() } override def b(): Unit = println("Actual implementation of b()") override def c(): Unit = println("Actual implementation of c()") }
测试用例
import org.mockito.MockitoSugar.{doNothing, mock, times, verify} import org.scalatest.FlatSpec class SUTObjectTest extends FlatSpec { behavior of "SUTObject.a()" it should "call b() and c() exactly once" in { val mockLogic = mock[SUTLogic] doNothing.when(mockLogic).b() doNothing.when(mockLogic).c() // 如果需要验证a()的逻辑,也可以将a()的实现移到特质的可测试方法中 // 或者进一步重构,让Object依赖特质实例,便于测试替换 mockLogic.a() verify(mockLogic, times(1)).b() verify(mockLogic, times(1)).c() } }
这种方式避免了直接Mock单例Object,既保证了测试隔离性,又符合Scala的面向特质编程理念。
总结
- 优先选择构造函数注入依赖特质,这是最符合SOLID原则、最利于长期维护的方案;
- 快速验证逻辑时可以用Mockito Spy,无需修改原代码;
- 对于Scala Object,通过特质提取逻辑来避免全局配置,提升测试的可维护性。
内容的提问来源于stack exchange,提问作者serge678
相关产品推荐
相关产品推荐

