Advanced Topics
First let's import the mocking functions for all our topics
import eu.monniot.scala3mock.ScalaMocks.*
Mocking overloaded, curried and polymorphic methods
Overloaded, curried and polymorphic methods can be mocked by specifying either argument types or type parameters.
Overloaded methods
trait Foo {
def overloaded(x: Int): String
def overloaded(x: String): String
def overloaded[T](x: T): String
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$1@4248959d
when(fooMock.overloaded(_: Int)).expects(10)
// res0: CallHandler1[Int, String] = <advanced-topics.md#L31> Foo.overloaded(10) once (never called - UNSATISFIED)
when(fooMock.overloaded(_: String)).expects("foo")
// res1: CallHandler1[String, String] = <advanced-topics.md#L31> Foo.overloaded(foo) once (never called - UNSATISFIED)
when(fooMock.overloaded[Double]).expects(1.23)
// res2: CallHandler1[Double, String] = <advanced-topics.md#L31> Foo.overloaded[T](1.23) once (never called - UNSATISFIED)
Polymorphic methods
trait Foo {
def polymorphic[T](x: List[T]): String
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$2@2fe36168
when(fooMock.polymorphic(_: List[Int])).expects(List(1, 2, 3))
// res3: CallHandler1[List[Int], String] = <advanced-topics.md#L58> Foo.polymorphic[T](List(1, 2, 3)) once (never called - UNSATISFIED)
Curried methods
trait Foo {
def curried(x: Int)(y: Double): String
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$3@2f042123
when(fooMock.curried(_: Int)(_: Double)).expects(10, 1.23)
// res4: CallHandler2[Int, Double, String] = <advanced-topics.md#L75> Foo.curried(10, 1.23) once (never called - UNSATISFIED)
The project have an action item to make the following statement compile:
trait Foo {
def curried(x: Int)(y: Double): String
}
val fooMock = mock[Foo]
when(fooMock.curried)
// java.lang.ClassCastException: class eu.monniot.scala3mock.functions.MockFunction2 cannot be cast to class eu.monniot.scala3mock.functions.MockFunction1 (eu.monniot.scala3mock.functions.MockFunction2 and eu.monniot.scala3mock.functions.MockFunction1 are in unnamed module of loader sbt.internal.LayeredClassLoader @104d5261)
// at repl.MdocSession$MdocApp.$init$$$anonfun$1(advanced-topics.md:93)
At the moment the when
macro isn't smart enough to recognize a curried function whereas the mock
macro is. That result in the underlying mock correctly creating a MockFunction2[Int, Double, String]
(two args) but the when
macro exposes a MockFunction1[Int, Double => String]
(one arg).
Methods with implicit parameters
This case is very similar to curried methods. All you need to do is to help the Scala compiler know that memcachedMock.get
should be converted to MockFunction2
. For example:
class Codec()
trait Memcached {
def get(key: String)(implicit codec: Codec): Option[Int]
}
val memcachedMock = mock[Memcached]
// memcachedMock: Memcached = repl.MdocSession$MdocApp$MemcachedMock$1@79b5caf9
implicit val codec = new Codec
// codec: Codec = repl.MdocSession$MdocApp$Codec$1@3ac947b3
when(memcachedMock.get(_ : String)(_ : Codec)).expects("some_key", *).returning(Some(123))
// res5: CallHandler2[String, Codec, Option[Int]] = <advanced-topics.md#L109> Memcached.get(some_key, *) once (never called - UNSATISFIED)
Repeated parameters
Repeated parameters are represented as a Seq. For example, given:
trait Foo {
def takesRepeatedParameter(x: Int, ys: String*): Unit
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$5@3ed6b7ae
when(fooMock.takesRepeatedParameter).expects(42, Seq("red", "green", "blue"))
// res6: CallHandler2[Int, Seq[String], Unit] = <advanced-topics.md#L130> Foo.takesRepeatedParameter(42, List(red, green, blue)) once (never called - UNSATISFIED)
Returning values (onCall)
By default mocks and stubs return null
. You can return predefined value using the returning
method. When the returned value depends on function arguments, you can return the computed value (or throw a computed exception) with onCall
. For example:
trait Foo {
def increment(a: Int): Int
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$6@4c0fce1a
when(fooMock.increment).expects(12).returning(13)
// res7: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(12) once (called once)
assert(fooMock.increment(12) == 13)
when(fooMock.increment).expects(*).onCall { (arg: Int) => arg + 1}
// res9: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(*) once (called once)
assert(fooMock.increment(100) == 101)
when(fooMock.increment).expects(*).onCall { arg => throw new RuntimeException("message") }
// res11: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(*) once (called once)
try {
fooMock.increment(0)
false
} catch {
case _: RuntimeException => true
case _ => false
}
// res12: Boolean = true
val mockIncrement = mockFunction[Int, Int]
// mockIncrement: MockFunction1[Int, Int] = MockFunction1
mockIncrement.expects(*).onCall { (arg: Int) => arg + 1 }
// res13: CallHandler1[Int, Int] = MockFunction1(*) once (called once)
assert(mockIncrement(10) == 11)
Call count
By default, mocks expect exactly one call. Alternative constraints can be set with repeated
:
val mockedFunction = mockFunction[Int, Int]
// mockedFunction: MockFunction1[Int, Int] = MockFunction1
mockedFunction.expects(42).returns(42).repeated(3 to 7)
// res15: CallHandler1[Int, Int] = MockFunction1(42) between 3 and 7 times (never called - UNSATISFIED)
mockedFunction.expects(3).repeated(10)
// res16: CallHandler1[Int, Int] = MockFunction1(3) between 10 and 2147483646 times (never called - UNSATISFIED)
There are various aliases for common expectations:
val mockedFunction1 = mockFunction[Int, String]
// mockedFunction1: MockFunction1[Int, String] = MockFunction1
val mockedFunction2 = mockFunction[Int, String]
// mockedFunction2: MockFunction1[Int, String] = MockFunction1
val mockedFunction3 = mockFunction[Int, String]
// mockedFunction3: MockFunction1[Int, String] = MockFunction1
val mockedFunction4 = mockFunction[Int, String]
// mockedFunction4: MockFunction1[Int, String] = MockFunction1
val mockedFunction5 = mockFunction[Int, String]
// mockedFunction5: MockFunction1[Int, String] = MockFunction1
mockedFunction1.expects(1).returning("foo").once
// res17: CallHandler1[Int, String] = MockFunction1(1) once (called once)
mockedFunction2.expects(2).returning("foo").noMoreThanTwice
// res18: CallHandler1[Int, String] = MockFunction1(2) no more than twice (never called)
mockedFunction3.expects(3).returning("foo").repeated(3)
// res19: CallHandler1[Int, String] = MockFunction1(3) between 3 and 2147483646 times (called 3 times)
mockedFunction4.expects(4).returning("foo").repeated(atLeast = 1, atMost = 2)
// res20: CallHandler1[Int, String] = MockFunction1(4) between 1 and 2 times (called twice)
mockedFunction5.expects(5).returning("foo").exactly(2)
// res21: CallHandler1[Int, String] = MockFunction1(5) twice (called twice)
mockedFunction5.expects(6).returning("foo").never
// res22: CallHandler1[Int, String] = MockFunction1(6) never (never called)
mockedFunction1(1)
// res23: String = "foo"
mockedFunction3(3)
// res24: String = "foo"
mockedFunction3(3)
// res25: String = "foo"
mockedFunction3(3)
// res26: String = "foo"
mockedFunction4(4)
// res27: String = "foo"
mockedFunction4(4)
// res28: String = "foo"
mockedFunction5(5)
// res29: String = "foo"
mockedFunction5(5)
// res30: String = "foo"
For a complete list, see handlers.CallHandler
.
Exceptions
Instead of returning a value, mock can be instructed to throw an exception. This can be achieved either by throwing an exception in onCall
or by using the throws
method.
trait Foo {
def increment(a: Int): Int
}
val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$7@380173a2
// Using the throws or throwing function
when(fooMock.increment).expects(5).throws(new RuntimeException("message"))
// res31: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(5) once (called once)
when(fooMock.increment).expects(6).throwing(new RuntimeException("message"))
// res32: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(6) once (never called - UNSATISFIED)
try {
fooMock.increment(5)
false
} catch {
case _: RuntimeException => true
case _ => false
}
// res33: Boolean = true
// Throwing in an onCall definition
when(fooMock.increment).expects(*).onCall { (i) =>
if(i==0) throw new RuntimeException("i == 0")
else i + 1
}
// res34: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(*) once (called once)
try {
fooMock.increment(0)
false
} catch {
case _: RuntimeException => true
case _ => false
}
// res35: Boolean = true
Argument Capture
ScalaMock support capturing argument when using mocks. This allow the usage of MatchAny
while asserting the argument after the fact.
Scala3Mock doesn't current support this feature, although nothing in the library actively prevent its inclusion. Contribution welcome.
See https://github.com/fmonniot/scala3mock/issues/5 if you'd like to help.
Mocking 0-parameter functions
Mocking methods which have zero parameters or are parameter-less are a bit different. The when
macro take a reference to the function without applying it. In the case of parameterless functions, such reference is indiguishable from applying it. Zero-parameters function could theoritically be seen as different, but that could lead to confusion (and indeed, it was legal in Scala 2 and turned out to be confusing).
To go around this, you can simply wrap the method into a () => mock.method
function:
trait Test {
def parameterless: Int
def zeroParameter(): Int
}
val m = mock[Test]
// m: Test = repl.MdocSession$MdocApp$TestMock$1@20d10dcb
when(() => m.zeroParameter()).expects().returns(1)
// res36: CallHandler0[Int] = <advanced-topics.md#L374> Test.zeroParameter() once (never called - UNSATISFIED)
when(() => m.parameterless).expects().returns(1)
// res37: CallHandler0[Int] = <advanced-topics.md#L374> Test.parameterless() once (never called - UNSATISFIED)