是否可以模拟Scala隐式类?

时间:2018-12-04 17:15:10

标签: scala mockito implicit

我可以通过隐式类使用其他方法扩展Scala类Foo

trait Foo {
  def bar: String
}

object FooExtensions {
  object implicits {
    implicit class FooOps(foo: Foo) {
      def baz: String = "baz"
    }
  }
}

但是我可以模拟那些方法吗?

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

class MySpec extends WordSpec with MockitoSugar {
  "My mock" should {
    "handle methods from implicit classes" in {
      import FooExtensions.implicits._
      val foo = mock[Foo]
      Mockito.when(foo.baz).thenReturn("bix") // fails at runtime
    }
  }
}

这会编译,但会失败

when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

是否可以模拟通过隐式类添加的方法?希望能与Mockito(或mockito-scala)在一起,但我对任何可行的方法都感兴趣。

2 个答案:

答案 0 :(得分:3)

关于扩展方法,基本上就是语法糖:

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

foo.bar

等于

new ExtensionMethods(foo).bar

所以嘲笑:

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

成为:

Mockito.when(new ExtensionMethods(foo).bar).thenReturn("bix")

我认为没有解决方法-也许PowerMock可以让您更改类构造函数...,但是使用普通的Mockito不可能。

通常,这不是问题。那是因为:

  • 您将扩展方法的行为置于行为中,这仅取决于扩展值和传递的参数(扩展方法通常是不需要模拟的纯函数)-如果您想在那里进行更改,则可以更改输入,
  • 如果行为应更改,则可以在类型类中实现它,并使扩展方法使用该类型类来注入行为

    trait Bar {
      def bar: String
    }
    object Bar {
      implicit val defaultBar: Bar = new Bar { def bar = "bar" }
    }
    
    implicit class ExtensionMethods(foo: Foo) {
      def bar(implicit bar: Bar): String = bar.bar
    }
    
    // in test
    implicit var overridenBar: Bar = ...
    assert(foo.bar === "sth")
    

附带说明:功能越多,模拟事物就越少,因为一切都将仅取决于传递给内部的输入,而一连串的模拟只会变成代码的味道-紧密耦合,问题是许多Java库甚至都没有遵循SOLID原则,这使得它们既难以与FP一起使用/测试,又很难单独使用OOP。我是在告诉您这种情况,以防您觉得嘲笑是唯一的解决方法。

答案 1 :(得分:0)

实现此目标的唯一方法是使用隐式转换而不是隐式类

这是一个旨在说明如何实现此目的的黑客工具,但我强烈建议您看一下代码,看看为什么您实际上需要这样做

因此,按照您的示例,您可以将代码修改为如下所示

trait Foo {
  def bar: String
}

object FooExtensions {
  object implicits {
    implicit fooToOps(foo: Foo): FooOps = new FooOps(foo)
    class FooOps(foo: Foo) {
      def baz: String = "baz"
    }
  }
}

和您的测试

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[FooOps]
        implicit fooToOps(foo: Foo): FooOps = fooOps
        val foo = mock[Foo]
        when(foo.baz) thenReturn "bix" // works
      }
    }
  }

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

正如我所说,您可以像这样使它工作,但是我同意Mateusz的观点,您不需要