使用Mockito mock含原始类型的Scala case类时触发空指针异常
嘿,这个问题我之前踩过一模一样的坑!让我给你好好捋捋来龙去脉,还有可行的解决办法。
先还原下你的场景:
你定义了这样的 trait 和 case class:
trait HasId[T] { def id: T } case class Entity(id: Long) extends HasId[Long]
然后在测试里尝试 mock Entity 并 stub id 属性:
val entityMock = mock[Entity] Mockito.when(entityMock.id).thenReturn(0)
结果执行到第二行直接抛出了NPE,报错日志是:
An exception or error caused a run to abort.
java.lang.NullPointerException
at com.test.Entity$MockitoMock$1085095743.id(Unknown Source)
at com.test.Test.(Test.scala:23)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)...
为啥会这样?核心原因是Scala 对原始类型的编译处理和 Mockito 的 mocking 逻辑不兼容:
- Scala 里的
Long会被编译成 Java 的long原始类型,case class 的idgetter 方法返回值也是原始类型long。 - Mockito 在生成 mock 实例时,对于原始类型的方法,没法像引用类型那样返回 null 作为临时占位值——因为原始类型不能为 null。所以当你调用
entityMock.id准备 stub 的时候,直接触发了 NPE,连when(...)逻辑都没走到。 - 你换成
String没问题,是因为String是引用类型,Mockito 可以返回 null 临时占位,后续的thenReturn能正常覆盖这个值。
方案1:换用 ScalaMock(最推荐)
Mockito 本来就是为 Java 设计的,对 Scala 的 case class、值类这些特性支持一直不太友好。ScalaMock 是专门为 Scala 打造的 mocking 框架,能完美处理这些 Scala 特有的语法,用它来 mock Entity 就不会有这个问题。
示例代码:
import org.scalamock.scalatest.MockFactory import org.scalatest.flatspec.AnyFlatSpec class EntityTest extends AnyFlatSpec with MockFactory { trait HasId[T] { def id: T } case class Entity(id: Long) extends HasId[Long] "Mocked Entity" should "return the specified id" in { val entityMock = mock[Entity] (entityMock.id _).expects().returning(0L) assert(entityMock.id == 0L) } }
方案2:把原始类型换成包装类型
如果不想换框架,可以把 Entity 里的 Long 换成 Java 的 java.lang.Long(或者 Scala 的 Option[Long],如果业务允许 id 为空的话),这样 getter 方法返回的是引用类型,Mockito 可以安全返回 null 作为临时值,后续的 thenReturn 也能正常生效:
// 修改后的 Entity case class Entity(id: java.lang.Long) extends HasId[java.lang.Long] // 测试代码不变,正常运行 val entityMock = mock[Entity] Mockito.when(entityMock.id).thenReturn(0L)
方案3:Mock trait 而非具体的 case class
既然 Entity 实现了 HasId[Long],你可以直接 mock 这个 trait,绕开 case class 的原始类型 getter 问题:
val idMock = mock[HasId[Long]] Mockito.when(idMock.id).thenReturn(0L) // 如果业务代码需要 Entity 类型,可以做类型转换(前提是代码依赖的是 HasId 而非具体 Entity) val entity: Entity = idMock.asInstanceOf[Entity]
不过这个方案有局限性,如果你的代码必须依赖 Entity 具体类型,那可能不太适用。
内容的提问来源于stack exchange,提问作者mnowak

