8

Full disclosure: I'm very new to mocking and mocking frameworks. I'm trying to use ScalaMock because it seemed like the 'default' mocking framework to use with ScalaTest but I am happy to use any other framework which is compatible with ScalaTest.

The problem: I've written in Scala a class that talks to a socket. The class has a type parameter of what sort of socket it is to talk to and one of it's arguments is a factory for creating sockets of that type. It has the signature:

class XScanner[T <: SocketClient](
  confPath: String = "/etc/default/configPath",
  socketClientFactory: String => T
) extends ScannerBase(path)

I would like to be able to write unit tests for this class by supplying a mock SocketClient so my test code doesn't have to connect to a real socket but I can't work out how to do this with ScalaMock.

My test code looks like this:

val t = new XScanner[SocketClient](confPath, (s: String) => mock[SocketClient])

Clearly that won't compile because SocketClient expects a path to the socket as an argument but I can't call mock[SocketClient(s)] because that's not a type and I can't call mock[SocketClient](s) because mock doesn't take the arguments of the type passed to it as it's own arguments.

So how can I write a mock SocketClient factory to pass to my Scanner? I can't even work out how to mock a class that takes arguments!

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
jangoolie
  • 125
  • 1
  • 1
  • 4

1 Answers1

4

The insight is that what you need to mock is socketClientFactory. And then set it up to return a mock SocketClient.

Given:

trait SocketClient {
  def frobnicate(): Unit
}

class ScannerBase(path: String)

class XScanner[T <: SocketClient](
  confPath: String = "/etc/default/configPath",
  socketClientFactory: String => T
) extends ScannerBase(confPath) {
  val socket = socketClientFactory("Some Socket Name")
  socket.frobnicate
}

(side note - your default value for confPath can never be used because there's no default value for socketClientFactory).

then this should get you started (this is with Scala 2.9.x and ScalaMock2 - 2.10.x with ScalaMock3 will be slightly different, but not much so):

import org.scalatest.FunSuite
import org.scalamock.scalatest.MockFactory
import org.scalamock.generated.GeneratedMockFactory

class ScannerTest extends FunSuite with MockFactory with GeneratedMockFactory {

  test("scanner") {
    val mockFactory = mockFunction[String, SocketClient]
    val mockClient = mock[SocketClient]
    mockFactory.expects("Some Socket Name").returning(mockClient)
    mockClient.expects.frobnicate
    val scanner = new XScanner("path/to/config", mockFactory)
  }
}

For completeness, here's the same test in Scala 2.10.0 and ScalaMock3:

import org.scalatest.FunSuite
import org.scalamock.scalatest.MockFactory

class ScannerTest extends FunSuite with MockFactory {

  test("scanner") {
    val mockFactory = mockFunction[String, SocketClient]
    val mockClient = mock[SocketClient]
    mockFactory.expects("Some Socket Name").returning(mockClient)
    (mockClient.frobnicate _).expects()
    val scanner = new XScanner("path/to/config", mockFactory)
  }
}
Paul Butcher
  • 10,722
  • 3
  • 40
  • 44
  • 1
    I'm not sure how this overcomes the fact that SocketClient takes 1 argument. I can't write: val mockClient = mock[SocketClient] mockFactory.expects("Some Socket Name").returning(mockClient) Because mock[SocketClient] won't compile. Are you suggesting I modify SocketClient to take no arguments or have default values for all the arguments? Even if no defaults make sense. Thanks very much for replying btw. – jangoolie Jan 13 '13 at 23:54
  • 1
    Ah, sorry - I missed that wrinkle first time around. Right now, ScalaMock3 can only mock traits and no-args classes. But if you control SocketClient, you should still be fine - create a trait which defines the methods and implement it in your class: trait SocketClientBase; class SocketClient(...) extends SocketClientBase. Then you can mock SocketClientBase and you should be good? Or am I missing something? – Paul Butcher Jan 14 '13 at 01:01
  • Also of note Even if I copy your code exactly I can't get it to compile. I get: overriding method nestedSuites in trait SuiteMixin of type => scala.collection.immutable.IndexedSeq[org.scalatest.Suite]; [error] method nestedSuites in trait Suite of type => List[org.scalatest.Suite] has incompatible type [error] class ScannerTest extends FunSuite with MockFactory { – jangoolie Jan 14 '13 at 01:06
  • That's seriously weird - I cut and paste the code in my answer directly from a working project. Which version of ScalaTest are you using? Can you make a project available (on GitHub?) which demonstrates the problem? – Paul Butcher Jan 14 '13 at 01:25
  • Ok I created an example project and put it on github: https://github.com/jangoolie/scalaMockExample All I did was copy the code from your answer and add a build.sbt file. – jangoolie Jan 14 '13 at 02:32
  • Thanks. The problem was conflicting ScalaTest versions - ScalaMock3 depends on ScalaTest 2.0 or higher, and you were using 1.9.1. What I'm confused about is that sbt doesn't give any kind of warning about the conflict - I'll see if I can get the sbt guys to let me know why. – Paul Butcher Jan 14 '13 at 10:07
  • Yup that got it working. Now I just need to work out how to actually use mock objects... Thanks for all your help. – jangoolie Jan 15 '13 at 03:39