【问题标题】:How to avoid duplication of mocked functions signatures in scalamock?如何避免在 scalamock 中重复模拟函数签名?
【发布时间】:2018-03-15 23:59:40
【问题描述】:

我正在使用 scalamock 来模拟这个类:

class HttpService {
  def post[In, Out]
    (url: String, payload: In)
    (implicit encoder: Encoder[In], decoder: Decoder[Out])
    : Future[Out] = ...
  ...
}

...所以我的测试类有一个这样使用的模拟:

val httpService = mock[HttpService]

(httpService.post[FormattedMessage, Unit](_ : String, _ : FormattedMessage) (_ : Encoder[FormattedMessage], _: Decoder[Unit]))
          .expects("http://example.com/whatever",*, *, *)
          .returning(Future.successful(()))

显然我必须编写整个模拟函数签名。如果我只将下划线放在签名中,没有相应的类型,我会得到这样的错误:

[error] missing parameter type for expanded function ((x$1: <error>, x$2, x$3, x$4) => httpService.post[FormattedMessage, Unit](x$1, x$2)(x$3, x$4)) 
[error]       (httpService.post[FormattedMessage, Unit](_, _) (_, _))
                                                        ^

我不喜欢这段代码的地方是,在测试中的多个地方都使用了模拟期望,并且这个丑陋的签名在所有地方重复,但具有不同的 In/Out 类型参数和期望。

所以我想我会写一个类

class HttpServiceMock extends MockFactory {
  val instance = mock[HttpService]
  def post[In, Out] = instance.post[In, Out](_ : String, _ : In) (_ : Encoder[In], _: Decoder[Out])
}

...并像这样使用它:

val httpService = new HttpServiceMock()

...

httpService.post[FormattedMessage, Unit]
      .expects("http://example.com/whatever",*, *, *)
      .returning(Future.successful(()))

...编译得很好,但是当我运行测试时出现以下错误:

java.lang.NoSuchMethodException: com.myapp.test.tools.HttpServiceMock.mock$post$0()
  at java.lang.Class.getMethod(Class.java:1786)
  at com.myapp.controllers.SlackControllerSpec.$anonfun$new$3(SlackControllerSpec.scala:160)
  at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
  at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:83)
  at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
  at org.scalatest.Transformer.apply(Transformer.scala:22)
  at org.scalatest.Transformer.apply(Transformer.scala:20)
  at org.scalatest.WordSpecLike$$anon$1.apply(WordSpecLike.scala:1078)
  at org.scalatest.TestSuite.withFixture(TestSuite.scala:196)
  at org.scalatest.TestSuite.withFixture$(TestSuite.scala:195)

如何解决此错误?还有其他方法可以避免一遍又一遍地重写模拟函数签名吗?

更新: 最后的模拟看起来像这样:

trait HttpServiceMock extends MockFactory {
  object httpService {
    val instance = mock[HttpService]

    def post[In, Out] = toMockFunction4(instance.post[In, Out](_: String, _: In)(_: Encoder[In], _: Decoder[Out]))
  }
}

【问题讨论】:

  • 你有没有试过通过隐式转换来定义这个快捷方式?我的意思是implicit class MockExt(val service: mock[HttpService]) { def post[In, Out] = service.post(...) }

标签: scala scalamock


【解决方案1】:

您可以使用以下代码:

trait HttpMockSupport {
  this: MockFactory =>
  val httpService = mock[HttpService]

  def prettyPost[In, Out]: MockFunction4[String, In, Encoder[In], Decoder[Out], Future[Out]] = {
    toMockFunction4(httpService.post[In, Out](_: String, _: In)(_: Encoder[In], _: Decoder[Out]))
  }
}

class AClassThatNeedsHttpServiceMocking extends FreeSpec with Matchers with MockFactory with HttpMockSupport {

  "HttpService should post" in {

    val url = "http://localhost/1"
    val input = "input"
    implicit val encoder: Encoder[String] = new Encoder[String] {}
    implicit val decoder: Decoder[String] = new Decoder[String] {}

    prettyPost[String, String]
      .expects(url, input, encoder, decoder)
      .returns(Future.successful("result"))

    httpService.post(url, input)
  }
}

它将常见的模拟放在一个特征中,该特征可以在所有需要模拟 HttpService 的地方进行扩展,并且只需调用不难看的方法:)

更新 1:

更新它以接受预期的参数。

更新 2:

将 prettyPost 方法更新为通用方法,以便我们可以设置任何类型的期望。

Scalamock 需要一个 MockFunctionX。因此,在您的情况下,您所要做的就是将丑陋的函数转换为漂亮的函数,然后将其转换为 MockFunctionX。

【讨论】:

  • 这不行,因为对于某些用途,我想为每次通话设置不同的期望。所以基本上我的模拟应该以某种方式支持我为每种用法设置不同期望的事实
  • 这应该不是问题。我已更新代码以接受您想要设置期望的参数列表。
  • 如谓词或 epsilon 等更高级的匹配呢? scalamock.org/user-guide/matching 还要验证重复调用呢?这种方法极大地限制了“模拟验证”
  • 我想我理解您的要求。你只是不喜欢到处都是丑陋的语法,但你需要 scalamock 的全部功能。我已经更新了答案以使该函数完全通用,以便您可以对其执行任何 scalamock 操作。
  • 没错!我需要 scalamock 的全部功能,但我想将“丑陋”保留在一个地方,以便测试很好且可读。
【解决方案2】:

不要使用 Scalamock,将 HttpService 设为 trait 并直接实现 trait 来模拟你需要的任何东西。例如。 (您可以将其粘贴到 Scala REPL 中,但请记住在最后按 EnterCtrl+D):

:rese
:pa
import scala.concurrent.Future

trait Encoder[A]
trait Decoder[A]

// HttpService.scala

trait HttpService {
  def post[In: Encoder, Out: Decoder](
    url: String, payload: In): Future[Out]
}

object HttpService extends HttpService {
  override def post[In: Encoder, Out: Decoder](
    url: String,
    payload: In):
    Future[Out] = ???
}

// HttpServiceSpec.scala

class Mock[Out](result: Future[Out]) extends HttpService {
  override def post[In: Encoder, Out: Decoder](
    url: String,
    payload: In):
    Future[Out] =
    // This is fine because it's a mock.
    result.asInstanceOf[Future[Out]]
}

【讨论】:

  • 我需要 ScalaMock 来设置模拟调用的各种期望,例如参数值、重复次数等。我不想自己实现调用验证的逻辑
  • @vidi 很公平,我仍然建议先将HttpService 设为特征,然后再对其进行嘲笑。更好的设计是使其成为具有 GetPost 等子类的 ADT。这将更加灵活。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-05
  • 1970-01-01
  • 2014-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多