Features
If you are not using effects systems - all scalamock features are brought into scope via org.scalamock.stubs.Stubs
which your suite should extend.
There are:
stub[T]
method, which allow macro generation of corresponding objects- Implicit conversions from method function representation to different
StubbedMethod
s, which allows to set method results and get invoked method arguments resetStubs()
method allowing to clear all stubs in case you are using them per suite.
ETA expansion
Scalamock uses ETA expansion to convert method selection into a function. And function is converted into corresponding StubbedMethod
representation via implicit conversion, when some method from it used, e.g. returnsWith
.
Examples:
class Foo:
def noArgs: String = ""
def oneArg(x: Int): String = ""
def twoArgs(x: Int, y: String): String = ""
val foo = Foo()
val noArgsFun: () => String = () => foo.noArgs
val oneArgFun: Int => String = foo.oneArg
val twoArgsFun: (Int, String) => String = foo.twoArgs
In Scala 2 you should also add
_
to convert method with arguments to a function. E.G.myObject.myMethod _
For complex methods you can consult with FAQ page.
Generating a stub
Method stub[T]
allows you to generate a stub object for any trait. It returns Stub[T]
type which is subtype of T
. If you want to specify type explicitly - you should use only Stub[T]
and not T
.
Otherwise, you will get compilation errors while setting up method results.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.Stubs
trait MyTrait
class MySpec extends munit.FunSuite, Stubs {
test("my test case"):
val myTrait: Stub[MyTrait] = stub[MyTrait]
}
StubbedMethod
StubbedMethod allows you to do 4 simple things:
- Set up method result. You can also set different result based on arguments
- Get number of times method was invoked
- Get method arguments with which it was invoked
- Check order with some other StubbedMethod
NotImplementedError
If you don’t set up method result it will throw NotImplementedError
with clear stack trace and method description. Method description may vary a bit depending on scala version.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.Stubs
case class User(id: Long)
trait UserService:
def findUser(userId: Long): Option[User]
class MySuite extends munit.FunSuite, Stubs {
test("findUserExample"):
val userService = stub[UserService]
userService.findUser(100)
}
Output will be something like:
scala.NotImplementedError: Implementation is missing for [<stub-1> UserService.findUser(userId: Long)Option[User]]
at org.scalamock.stubs.StubbedMethod$Internal.impl(StubbedMethod.scala:273)
at Playground$MySuite$anon$1.findUser(main.scala:12)
returnsWith
returnsWith
allows you to set method result no matter what arguments are.
This is the default way if you don’t use abstraction for tests or create your stubs per suite.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.Stubs
case class User(id: Long)
trait UserService:
def findUser(userId: Long): Option[User]
class MySuite extends munit.FunSuite, Stubs:
val userId = 100
val user = User(userId)
test("findUserExample"):
val userService = stub[UserService]
userService.findUser.returnsWith(Some(user))
assertEquals(userService.findUser(userId), Some(user))
returns
returns
allows you to set method result depending on arguments.
This may be useful if you want to abstract over your tests or create your stubs per suite
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.*
case class User(id: Long)
trait UserService:
def findUser(userId: Long): Option[User]
class MySuite extends munit.FunSuite, Stubs:
val knownUserId = 100
val unknownUserId = -1
val user = User(userId)
val userService: Stub[UserService] = stub[UserService]
userService.findUser.returns:
case `knownUserId` => Some(user)
case _ => None
override def beforeEach(context: BeforeEach): Unit =
resetStubs()
test("return user for known user id"):
assertEquals(userService.findUser(knownUserId), Some(user))
test("not return user for unknown user id"):
assertEquals(userService.findUser(unknownUserId), None)
calls and times
calls
returns arguments with which method was invoked, since there might be multiple invocations, it returns a list.
- If method has no arguments - it returns a list of
()
values. - If method has one argument - it returns a list of that argument type.
- If method has multiple arguments - it returns a list of tupled arguments.
times
returns number of times method was invoked. This is simply size of calls
Currently
StubbedMethod0[R]
not supportscalls
, in future versions it will becomeStubbedMethod[Unit, R]
and will support it.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.Stubs
case class User(id: Long)
trait UserService:
def findUser(userId: Long): Option[User]
class MySuite extends munit.FunSuite, Stubs:
val userId = 100
val user = User(userId)
test("findUserExample"):
val userService = stub[UserService]
userService.findUser.returnsWith(Some(user))
userService.findUser(userId)
userService.findUser(userId)
assertEquals(userService.findUser.calls, List(userId, userId))
assertEquals(userService.findUser.times, 2)
isBefore and isAfter
isBefore
and isAfter
methods allow you to check order of method invocations.
They use CallLog
for that which should be defined before stub
generation as a context parameter.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.*
trait MyService:
def firstMethod(id: Long): Option[String]
def secondMethod(phone: String): Unit
class MySuite extends munit.FunSuite, Stubs {
test("my test case") {
given CallLog = CallLog()
val myService = stub[MyService]
myService.firstMethod.returnsWith(Some("some result"))
myService.secondMethod.returnsWith(())
assertEquals(myService.secondMethod.isBefore(myService.firstMethod), false)
assertEquals(myService.secondMethod.isAfter(myService.firstMethod), true)
}
}
You can also print your call log after running your tests to debug order of calls. Just use toString
for that.
convert to StubbedMethod explicitly
You can also convert to StubbedMethod
explicitly using stubbed
method or adding explicit type annotation StubbedMethod
and use it later.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalameta::munit:1.1.1
import org.scalamock.stubs.Stubs
case class User(id: Long)
trait UserService:
def findUser(userId: Long): Option[User]
def eraseAll(): Unit
class MySuite extends munit.FunSuite, Stubs:
val userId = 100
val user = User(userId)
test("findUserExample"):
val userService = stub[UserService]
val findUserStub = stubbed(userService.findUser)
//val findUserStub: StubbedMethod[Long, Option[User]] = userService.findUser
findUserStub.returnsWith(Some(user))
val result = userService.findUser(userId)
assertEquals(result, Some(user))
assertEquals(findUserStub.times, 1)
assertEquals(findUserStub.calls, List(100))
Currently method without arguments is converted to
StubbedMethod0[R]
which is deprecated and later on will be replaced withStubbedMethod[Unit, R]
. So you should not explicitly use type annotation forStubbedMethod0[R]
Sharing your stubs or not
By default, stubs should not be shared between your test cases.
Usually you should create some fixture or wiring to be reused in each test-case or your test case should encapsulate this logic inside.
Let’s assume we have a Greetings functionality that we want to test. It will be very simple just to show you usage of fixtures.
trait Greetings:
def sayHello(name: String): Unit
object Greetings:
class Impl(formatter: Formatter):
def sayHello(name: String): Unit =
println(formatter.format(name))
trait Formatter:
def format(s: String): String
object EnglishFormatter extends Formatter:
def format(s: String): String = s"Hello $s"
object JapaneseFormatter extends Formatter:
def format(s: String): String = s"こんにちは $s"
Let’s now see how we can create a fixture. I’ll be using scalatest is this example:
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalatest::scalatest:3.2.19
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalamock.stubs.Stubs
class FormatterSpec extends AnyFunSuite, Matchers, Stubs:
class Env: // this is a fixture
val formatter = stub[Greetings.Formatter]
val greetings = Greetings.Impl(formatter)
test("test case 1"):
val env = Env()
env.formatter.format.returnsWith("Hello World")
env.greetings.sayHello("World")
env.formatter.format.times shouldBe 1
env.formatter.format.calls shouldBe List("World")
test("test case 2"):
val env = Env()
env.formatter.format.returnsWith("Hello Mama")
env.greetings.sayHello("Mama")
env.formatter.format.times shouldBe 1
env.formatter.format.calls shouldBe List("Mama")
Still, if you want to create your stubs once and share them between your test-cases - you can do it!
To make possible - you should use resetStubs()
method before or after each test case and ensure your test cases run sequentially.
//> using test.dep org.scalamock::scalamock:7.3.2
//> using test.dep org.scalatest::scalatest:3.2.19
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfter
import org.scalamock.stubs.Stubs
class FormatterSpec extends AnyFunSuite, Matchers, BeforeAndAfter, Stubs:
val formatter = stub[Greetings.Formatter]
val greetings = Greetings.Impl(formatter)
before {
resetStubs()
}
test("test case 1") {
formatter.format.returnsWith("Not Hello World")
greetings.sayHello("World") shouldBe "Not Hello World"
formatter.format.times shouldBe 1
formatter.format.calls shouldBe List("World")
}
test("test case 2") {
formatter.format.returnsWith("Hello Mama")
greetings.sayHello("Mama") shouldBe "Hello Mama"
formatter.format.times shouldBe 1
formatter.format.calls shouldBe List("Mama")
}