为什么我们不能在类变量上调用模拟方法?

时间:2015-11-03 15:22:59

标签: scala unit-testing mockito

我最近一直在学习Scala,现在,我正在尝试与Mockito进行一些学习测试,但我一直面临着一些非常奇怪的事情。

FooService.scala

Common Language Runtime Exceptions

FooServiceTest.scala

class FooService extends Serializable {

  val service: FooDaoTrait = FooDao
  val fooResult = service.getAllTheFoosFromDatabase     

  def putTheFoosInASet(): Set[String] = {
    fooResult.split(",").toSet
  }
}

所以在这里,当我运行测试时,Scala IDE会在我的类变量@RunWith(classOf[JUnitRunner]) class FooServiceTest extends FunSuite with MockitoSugar { val m = mock[FooDaoTrait] when(m.getAllTheFoosFromDatabase).thenReturn("Hi, my name is foo") val instance = new FooService{ override val service = m } test("Let's get da Foos") { assert(instance.putTheFoosInASet().size === 3) // 3 is a random Value } } 上抛出唯一的java.lang.NullPointerException

但不知何故,当我在fooResult内移动FooResult时:

putTheFoosInASet()

没有抛出异常,一切运作良好...... 为什么以前的模式会让我异常?我不确定什么会导致这个错误,并且在不同的方法上调用 def putTheFoosInASet(): Set[String] = { val fooResult = service.getAlltheFoosFromDatabase fooResult.split(",").toSet } 并不是真正干净的imo。

任何意见将不胜感激。感谢。

1 个答案:

答案 0 :(得分:1)

简答:

这对mockito来说不是问题。要解决此问题,请使用lazy

class FooService extends Serializable {
  val service: FooDaoTrait = FooDao
  lazy val fooResult = service.getAllTheFoosFromDatabase    

// ... 

答案很长:

让我们忘记Mockito和Mocks。我们仍然可以重现这个问题:

val testFooDao = new FooDaoTrait {
  def getAllTheFoosFromDatabase() = "Hi, my name is foo"
}
val instance = new FooService { 
  override val service = testFooDao 
}
// ----> java.lang.NullPointerException

好的,那么发生了什么?添加一些打印件可以帮助我们理解:

class FooService extends Serializable {
  val service: FooDaoTrait = new FooDao("aa")
  println(s"Original FooService service is $service")
  val fooResult = service.getAllTheFoosFromDatabase
}

val testFooDao = new FooDaoTrait {
  def getAllTheFoosFromDatabase() = "Hi, my name is foo"
}
val instance = new FooService { 
  override val service = testFooDao 
  println(s"New FooService service is $service")
}

打印

// Original FooService service is null 
// NullPointerException ...

这就是我们获得NullPointerException的原因。由于service为空service.getAllTheFoosFromDatabase,因此给我们这个例外。

为什么会这样?

在scala中,严格的val(非延迟)的初始化按以下顺序进行:

  1. 超类在子类之前完全初始化。
  2. 否则,按声明顺序。
  3. 但是当val被覆盖时,它不会被多次初始化。虽然您的service属性定义了两次,但在构造超类(FooService)时,这个重写的val似乎为null。

    您可以找到更多信息here

    EXTRA

    通过你的榜样判断你的代码(不知道我能否; P),我有两个建议,希望你不要介意:

    首先,如果您将其作为类构造函数参数传递,我认为FooService.service会更好,更优雅。特别是如果你打算模拟服务,这就是人们通常这样做的方式(不仅在scala中,而且在其他语言中):

    class FooService(service: FooDaoTrait) {
    // ...
    

    其次,fooResult在我看来更像是对数据库的查询,因此它更适合成为一个函数(因为它应该在每次查询时都改变)

    class FooService extends Serializable {
      val service: FooDaoTrait = new FooDao("aa")
      def fooResult = service.getAllTheFoosFromDatabase
    // ...
    

    请注意,如果你采纳了这些建议(或两者兼而有之),你就不会遇到这个问题。