首页 > 解决方案 > 使用 ScalaMock 测试具有隐式类的类

问题描述

假设我有一个包含在 Try 块中的读取操作的特征:


import scala.util.Try

trait ReadingProvider[T] {
  def readTable(tableName: String):Try[T]
}

还有一个提供使用 spark 读取的方法的类和一个用于从故障中恢复的方法的隐式类


import org.apache.spark.sql._
import org.apache.spark.sql.types.StructType
import scala.util.{Try, Success, Failure}

class SparkReadingProvider(spark: SparkSession) extends ReadingProvider[DataFrame] {
  override readTable: Try[DataFrame] = Try(spark.read.table(tableName))

  def createEmptyDF(schema: StructType): DataFrame =spark.createDataFrame(spark.sparkContext.emptyRDD[Row], schema)

}

  implicit class ReadingHandler(tryDF: Try[DataFrame]) {

    def recoverWithEmptyDF(schema: StructType): DataFrame = tryDF match {
        case Failure(ex) => //Log something
          createEmptyDF(schema)
        case Success(df) => //Log something
          df
      }
  }
}

现在我有一个包含阅读和一些转换的对象:

object MyObject {

  def readSomeTable(tableName): SparkReadingProvider => DataFrame = provider => {
    import provider.ReadingHandler
    provider.readTable(tableName).recoverWithEmptyDF
    }

  def transform: DataFrame => DataFrame = ???

  def mainMethod(tableName)(implicit val provider: SparkReadingProvider): DataFrame =
    readSomeTable(tableName) andThen transform apply provider

}

我想对里面的方法进行单元测试MyObject。我不想使用真实的文件或表格,因此我的目标是使用模拟。

在我的测试中,我试图模拟 SparkReadingProvider:

describe("reading") {
  it("should return empty dataframe when reading failed") {
    val provider: SparkReadingProvider = mock[SparkReadingProvider]
    val tableName: String = "no_table"
    provider.readTable _ expects tableName returning Failure(new Exception("Table does not exist"))
    MyObject.readSomeTable(tableName) shouldBe empty
    }
}

但是它失败并出现错误:

意外调用:<mock-1> SparkReadingProvider.ReaderHandler(Failure(java.lang.Exception: 表不存在))

预期:inAnyOrder { < mock-1> SparkReadingProvider.readTable(no_table) 一次(调用一次)}

实际:<mock-1> SparkReadingProvider.readTable(no_table) <mock-1> SparkReadingProvider.ReaderHandler(Failure(java.lang.Exception: 表不存在))

我的问题是:

标签: scalaunit-testingapache-sparkmockingscalamock

解决方案


问题是扩展方法基本上是一种语法糖。

我将用一个例子来解释。

trait Foo
implicit class FooImplicit(foo: Foo) {
  def bar: String = "bar
}

foo.bar

被翻译成

new FooImplicit(foo).bar

如此嘲讽:

Mockito.when(foo.bar).thenReturn("bad")

变成:

Mockito.when(new FooImplicit(foo).bar).thenReturn("bad")

请注意如何foo.bar处理,这就是问题所在。

是否有可能在当前设置中实现我想要的?

不,我认为在当前设置中是不可能的。

如果没有,我应该如何重构我的代码

实现这一点的唯一方法是使用隐式转换而不是隐式类。

我将展示一个如何实现这一目标的示例:

trait Foo {
  def bar: String
}

object ImplicitFoo {
  object implicits {
    implicit fooToFooImplicit(foo: Foo): FooOps = new FooImplicit(foo)
    class FooImplicit(foo: Foo) {
      def bar: String = "bar"
    }
  }
}

和你的测试

import org.scalatest.WordSpec
import org.mockito.MockitoSugar

  class MySpec extends WordSpec with MockitoSugar {
    "My mock" should {
      "handle methods from implicit classes" in {
        val fooOps = mock[FooImplicit]
        implicit fooToOps(foo: Foo): FooImplicit = fooOps
        val foo = mock[Foo]
        when(foo.bar) thenReturn "bad" // works
      }
    }
  }

在您的生产中,您需要获取形状的隐式参数,Foo => FooImplicit因此当您从测试中调用该方法时,会提供实际的隐式模拟......

如果我在不同的类中测试隐式类中可用的方法,那么在 MyObject 中测试 readSomeTable 和 mainMethod 是否有意义?

我认为您不需要测试 MyObject 中的 readSomeTable 和 mainMethod。但其他方式是正确的。

让我知道它是否有帮助!


推荐阅读