0

I'm using scalamock to mock this class:

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

...so my test class has a mock used like this:

val httpService = mock[HttpService]

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

Apparently I have to write the whole mock function signature. If I only put the underscores in the signature, without the corresponding types, I get errors like this one:

[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](_, _) (_, _))
                                                        ^

What I don't like about this code is that the mock expectation is used in several places in the tests and this ugly signature is repeated all over the place but with different In/Out type parameters and expectations.

So I thought I would write a class

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

...and use it like this:

val httpService = new HttpServiceMock()

...

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

...which compiles fine but when I run the tests I get the following error:

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)

How can I fix this error? Are there other ways to avoid the re-writing of the mocked function signature over and over again?

UPDATE: In the end the mock looks like this:

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]))
  }
}
vidi
  • 2,056
  • 16
  • 34
  • Have you tried to define this shortcut method through an implicit conversion? I mean `implicit class MockExt(val service: mock[HttpService]) { def post[In, Out] = service.post(...) }` – laughedelic Mar 20 '18 at 04:21

2 Answers2

1

You can use the below code:

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)
  }
}

It puts the common mocking in a trait that can be extended in all the places that needs to mock HttpService and just call the non-ugly method :)

Update 1:

Updated it to accept the expected parameters.

Update 2:

Updated the prettyPost method to be generic so that we can set any kind of expectations.

Scalamock expects a MockFunctionX. So, in your case, all you have to do is to convert the ugly function to a pretty function and then convert it to a MockFunctionX.

James
  • 2,756
  • 17
  • 19
  • This is not ok because for some usages I want to setup the expectations different for every call. So basically my mock should support somehow the fact that I'm setting up different expectations for each usage – vidi Mar 20 '18 at 14:16
  • That shouldn't be a problem. I have updated the code to accept the list of parameters that you want to set expectations to. – James Mar 20 '18 at 14:29
  • what about more advanced matching like predicates or epsilon? http://scalamock.org/user-guide/matching/ Also what about verifying repeated calls? This approach limits the "mock verification" quite a lot – vidi Mar 20 '18 at 14:34
  • I think I understand your requirements. You just don't like the ugly syntax eveywhere but you need the full power of scalamock. I have updated the answer to make the function completely generic so you can perform any scalamock operations on it. – James Mar 20 '18 at 15:39
  • Exactly! I have the need for full power of scalamock but I want to keep the "ugliness" in a single place such that the tests are nice and readable. – vidi Mar 20 '18 at 17:45
0

Don't use Scalamock, make HttpService a trait and implement the trait directly to mock whatever you need. E.g. (you can paste this in the Scala REPL, but remember to press Enter and Ctrl+D at the end):

: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]]
}
L Y E S - C H I O U K H
  • 4,765
  • 8
  • 40
  • 57
Yawar
  • 11,272
  • 4
  • 48
  • 80
  • I need ScalaMock to setup various expectations for the mock calls like parameter values, the number of repeats etc. I don't want to implement the logic for calls verification by myself – vidi Mar 20 '18 at 14:16
  • @vidi fair enough, I would still recommend making `HttpService` a trait first and then mocking that. An even better design would be making it an ADT with subclasses for `Get`, `Post`, etc. This would be more flexible. – Yawar Mar 20 '18 at 14:32